locomotivecms-solid 0.2.2

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +10 -0
  5. data/Gemfile +6 -0
  6. data/LICENSE +20 -0
  7. data/README.md +152 -0
  8. data/Rakefile +7 -0
  9. data/lib/locomotivecms-solid.rb +2 -0
  10. data/lib/solid.rb +48 -0
  11. data/lib/solid/arguments.rb +26 -0
  12. data/lib/solid/block.rb +13 -0
  13. data/lib/solid/conditional_block.rb +35 -0
  14. data/lib/solid/context_error.rb +2 -0
  15. data/lib/solid/default_security_rules.rb +24 -0
  16. data/lib/solid/element.rb +51 -0
  17. data/lib/solid/engine.rb +4 -0
  18. data/lib/solid/extensions.rb +17 -0
  19. data/lib/solid/iterable.rb +18 -0
  20. data/lib/solid/liquid_extensions.rb +87 -0
  21. data/lib/solid/liquid_extensions/assign_tag.rb +21 -0
  22. data/lib/solid/liquid_extensions/for_tag.rb +102 -0
  23. data/lib/solid/liquid_extensions/if_tag.rb +44 -0
  24. data/lib/solid/liquid_extensions/unless_tag.rb +13 -0
  25. data/lib/solid/liquid_extensions/variable.rb +34 -0
  26. data/lib/solid/method_whitelist.rb +56 -0
  27. data/lib/solid/model_drop.rb +119 -0
  28. data/lib/solid/parser.rb +108 -0
  29. data/lib/solid/parser/ripper.rb +220 -0
  30. data/lib/solid/parser/ruby_parser.rb +88 -0
  31. data/lib/solid/tag.rb +11 -0
  32. data/lib/solid/template.rb +24 -0
  33. data/lib/solid/version.rb +3 -0
  34. data/locomotivecms-solid.gemspec +26 -0
  35. data/spec/solid/arguments_spec.rb +314 -0
  36. data/spec/solid/block_spec.rb +39 -0
  37. data/spec/solid/conditional_block_spec.rb +39 -0
  38. data/spec/solid/default_security_rules_spec.rb +180 -0
  39. data/spec/solid/element_examples.rb +67 -0
  40. data/spec/solid/liquid_extensions/assign_tag_spec.rb +27 -0
  41. data/spec/solid/liquid_extensions/for_tag_spec.rb +48 -0
  42. data/spec/solid/liquid_extensions/if_tag_spec.rb +64 -0
  43. data/spec/solid/liquid_extensions/unless_tag_spec.rb +54 -0
  44. data/spec/solid/liquid_extensions/variable_spec.rb +25 -0
  45. data/spec/solid/model_drop_spec.rb +26 -0
  46. data/spec/solid/parser/ripper_spec.rb +14 -0
  47. data/spec/solid/parser/ruby_parser_spec.rb +7 -0
  48. data/spec/solid/tag_spec.rb +26 -0
  49. data/spec/solid/template_spec.rb +37 -0
  50. data/spec/spec_helper.rb +8 -0
  51. data/spec/support/class_highjacker_examples.rb +33 -0
  52. data/spec/support/method_whitelist_matchers.rb +17 -0
  53. data/spec/support/parser_examples.rb +261 -0
  54. data/spec/support/tag_highjacker_examples.rb +33 -0
  55. metadata +204 -0
@@ -0,0 +1,108 @@
1
+ class Solid::Parser
2
+
3
+ BASE_PATH = File.join(File.expand_path(File.dirname(__FILE__)), 'parser')
4
+
5
+ begin
6
+ require 'ripper'
7
+ autoload :Ripper, File.join(BASE_PATH, 'ripper')
8
+ rescue LoadError
9
+ end
10
+ begin
11
+ require 'ruby_parser'
12
+ autoload :RubyParser, File.join(BASE_PATH, 'ruby_parser')
13
+ rescue LoadError
14
+ end
15
+
16
+ class ContextVariable < Struct.new(:name)
17
+ def evaluate(context)
18
+ Solid.to_liquid(context[name], context)
19
+ end
20
+ end
21
+
22
+ class Literal < Struct.new(:value)
23
+ def evaluate(context)
24
+ Solid.to_liquid(value, context)
25
+ end
26
+ end
27
+
28
+ class LiteralArray < Literal
29
+ def evaluate(context)
30
+ value.map{ |v| v.evaluate(context) }
31
+ end
32
+ end
33
+
34
+ class LiteralRange < Struct.new(:start_value, :end_value, :exclusive)
35
+ def evaluate(context)
36
+ Range.new(start_value.evaluate(context), end_value.evaluate(context), exclusive)
37
+ end
38
+ end
39
+
40
+ class LiteralHash < Literal
41
+ def evaluate(context)
42
+ Hash[value.map{ |k, v| [k.evaluate(context), v.evaluate(context)] }]
43
+ end
44
+ end
45
+
46
+ class MethodCall < Struct.new(:receiver, :name, :arguments)
47
+ include Solid::MethodWhitelist
48
+ BUILTIN_HANDLERS = {
49
+ :'&&' => ->(left, right) { left && right },
50
+ :'||' => ->(left, right) { left || right },
51
+ :'and' => ->(left, right) { left and right },
52
+ :'or' => ->(left, right) { left or right }
53
+ }
54
+ BUILTIN_HANDLERS.keys.each do |operator|
55
+ BUILTIN_HANDLERS[operator.to_s] = BUILTIN_HANDLERS[operator]
56
+ end
57
+
58
+ def evaluate(context)
59
+ Solid.to_liquid(pluck(receiver.evaluate(context), name, *arguments.map {|arg| arg.evaluate(context) }), context)
60
+ end
61
+
62
+ protected
63
+
64
+ def pluck(object, method, *args)
65
+ if BUILTIN_HANDLERS.has_key?(method)
66
+ BUILTIN_HANDLERS[method].call(object, *args)
67
+ elsif safely_respond_to?(object, method)
68
+ object.public_send(method, *args)
69
+ elsif object.respond_to?(:[]) && args.empty?
70
+ object[method]
71
+ elsif object.respond_to?(:before_method)
72
+ object.before_method(method, *args)
73
+ end
74
+ end
75
+
76
+ end
77
+
78
+ KEYWORDS = {
79
+ 'true' => Literal.new(true),
80
+ 'false' => Literal.new(false),
81
+ 'nil' => Literal.new(nil),
82
+ }
83
+
84
+ class << self
85
+
86
+ attr_writer :parser
87
+
88
+ def parser
89
+ @parser || begin
90
+ if defined?(Solid::Parser::RubyParser)
91
+ Solid::Parser::RubyParser
92
+ elsif defined?(Solid::Parser::Ripper)
93
+ Solid::Parser::Ripper
94
+ else
95
+ raise "You need to run MRI (to have Ripper), "\
96
+ "or have 'ruby_parser' in $LOAD_PATH "\
97
+ "or set #{self}.parser yourself"
98
+ end
99
+ end
100
+ end
101
+
102
+ def parse(string)
103
+ parser.parse(string)
104
+ end
105
+
106
+ end
107
+
108
+ end
@@ -0,0 +1,220 @@
1
+ class Solid::Parser::Ripper < Solid::Parser
2
+
3
+ def self.parse(string)
4
+ new(string).parse
5
+ end
6
+
7
+ def initialize(string)
8
+ @string = string
9
+ @sexp = nil
10
+ end
11
+
12
+ def parse
13
+ @sexp = ::Ripper.sexp(@string)
14
+ dive_in or raise 'Ripper changed?'
15
+ parse_one(@sexp)
16
+ end
17
+
18
+ # Looks for a structure like
19
+ # [:program, [[:array, [#stuff#]]]] or
20
+ # [:program, [[:array, nil]]]
21
+ def dive_in
22
+ @sexp = @sexp[1]
23
+ @sexp = @sexp.first
24
+ end
25
+
26
+ def parse_one(argument)
27
+ type = argument.shift
28
+ handler = "handle_#{type.to_s.sub('@', '')}"
29
+ raise Solid::SyntaxError, "unknown Ripper type: #{type.inspect}" unless respond_to?(handler)
30
+ public_send handler, *argument
31
+ end
32
+
33
+ # # spam
34
+ # [:@ident, "spam", [1, 33]] or
35
+ # # true
36
+ # [:@kw, "true", [1, 23]]
37
+ def handle_var_ref(var_ref)
38
+ parse_one(var_ref)
39
+ end
40
+
41
+ # # foo: 42
42
+ # [[:assoc_new, [:@label, "foo:", [1, 1]], [:@int, "42", [1, 5]]]]
43
+ def handle_bare_assoc_hash(assoc_hash)
44
+ LiteralHash.new assoc_hash.map {|(_, *key_value)| key_value.map(&method(:parse_one)) }
45
+ end
46
+
47
+ # # {foo: 42}
48
+ # [:assoclist_from_args, [[:assoc_new, [:@label, "foo:", [1, 2]], [:@int, "42", [1, 7]]]]]
49
+ def handle_hash(hash)
50
+ return LiteralHash.new({}) unless hash
51
+ handle_bare_assoc_hash(hash.last)
52
+ end
53
+
54
+ # # myvar.length
55
+ #
56
+ # [:var_ref, [:@ident, "myvar", [1, 1]]]
57
+ # :"."
58
+ # [:@ident, "length", [1, 7]]
59
+ def handle_call(receiver_sexp, method_call, method_sexp)
60
+ receiver = parse_one(receiver_sexp)
61
+ method = method_sexp[1]
62
+ MethodCall.new receiver, method, []
63
+ end
64
+
65
+ # # myvar
66
+ #
67
+ # since 1.9.3
68
+ # [:vcall, [:@ident, "myvar", [1, 0]]]
69
+ def handle_vcall(expression)
70
+ parse_one(expression)
71
+ end
72
+
73
+ # # myvar.split(',', 2)
74
+ #
75
+ # [:call, [:var_ref, [:@ident, "myvar", [1, 1]]], :".", [:@ident, "split", [1, 7]]]
76
+ # [:arg_paren, [:args_add_block, [
77
+ # [:string_literal, [:string_content, [:@tstring_content, ",", [1, 14]]]],
78
+ # [:@int, "2", [1, 18]]
79
+ # ], false]]
80
+ def handle_method_add_arg(call_sexp, args_sexp)
81
+ method_call = parse_one(call_sexp)
82
+ method_call.arguments = method_call_args(args_sexp)
83
+ method_call
84
+ end
85
+
86
+ # # args list: (',', 2)
87
+ # [:arg_paren, [:args_add_block, [
88
+ # [:string_literal, [:string_content, [:@tstring_content, ",", [1, 14]]]],
89
+ # [:@int, "2", [1, 18]]
90
+ # ], false]]
91
+ #
92
+ # 1 args list: ()
93
+ # [:arg_paren, nil]
94
+ def method_call_args(args_sexp)
95
+ return [] if args_sexp[1].nil?
96
+ args_sexp = args_sexp.last[1]
97
+ args_sexp.map(&method(:parse_one))
98
+ end
99
+
100
+ # # !true
101
+ # [:!, [:var_ref, [:@kw, "true", [1, 1]]]]
102
+ def handle_unary(operator, operand)
103
+ MethodCall.new(parse_one(operand), operator, [])
104
+ end
105
+
106
+ # # 1 + 2
107
+ # [:@int, "1", [1, 0]], :*, [:@int, "2", [1, 4]]
108
+ def handle_binary(left_operand, operator, right_operand)
109
+ receiver = parse_one(left_operand)
110
+ MethodCall.new(receiver, operator, [parse_one(right_operand)])
111
+ end
112
+
113
+ # # [1]
114
+ # [[:@int, "1", [1, 1]]
115
+ def handle_array(array)
116
+ LiteralArray.new((array || []).map(&method(:parse_one)))
117
+ end
118
+
119
+ # # (1)
120
+ # [[:@int, "42", [1, 2]]]
121
+ def handle_paren(content)
122
+ parse_one(content.first)
123
+ end
124
+
125
+ # # 1..10
126
+ # [[:@int, "1", [1, 0]], [:@int, "10", [1, 4]]]
127
+ def handle_dot2(start_value, end_value)
128
+ LiteralRange.new(parse_one(start_value), parse_one(end_value), false)
129
+ end
130
+
131
+ # # 1...10
132
+ # [[:@int, "1", [1, 0]], [:@int, "10", [1, 4]]]
133
+ def handle_dot3(start_value, end_value)
134
+ LiteralRange.new(parse_one(start_value), parse_one(end_value), true)
135
+ end
136
+
137
+ # # 'mystring'
138
+ # [:string_content, [:@tstring_content, "mystring", [1, 14]]]
139
+ # TODO: handle string interpolation
140
+ def handle_string_literal(string_content)
141
+ Literal.new(parse_one(string_content))
142
+ end
143
+
144
+ def handle_string_content(*parts)
145
+ parts.map(&method(:parse_one)).join
146
+ end
147
+
148
+ # [:@tstring_content, "mystring", [1, 14]]
149
+ def handle_tstring_content(string_content, lineno_column)
150
+ string_content
151
+ end
152
+
153
+ def handle_dyna_symbol(string_content)
154
+ Literal.new(string_content.first[1])
155
+ end
156
+
157
+ REGEXP_FLAGS = {
158
+ 'i' => Regexp::IGNORECASE,
159
+ 'x' => Regexp::EXTENDED,
160
+ 'm' => Regexp::MULTILINE,
161
+ 'n' => Regexp::NOENCODING
162
+ }
163
+
164
+ def instanciate_regexp(content, flags)
165
+ mode = 0
166
+ flags.each_char do |flag|
167
+ mode |= REGEXP_FLAGS[flag] || 0
168
+ end
169
+ Regexp.new(content, mode)
170
+ end
171
+
172
+ # # /bb|[^b]{2}/
173
+ # [[[:@tstring_content, "bb|[^b]{2}", [1, 1]]], [:@regexp_end, "/", [1, 4]]]
174
+
175
+ # # /bb|[^b]{2}/ix
176
+ # [[[:@tstring_content, "bb|[^b]{2}", [1, 1]]], [:@regexp_end, "/ix", [1, 4]]]
177
+
178
+ # TODO: handle regexp interpolation
179
+ def handle_regexp_literal(regexp_content, regexp_end)
180
+ Literal.new instanciate_regexp(regexp_content.first[1], regexp_end[1][1..-1])
181
+ end
182
+
183
+ # # true
184
+ # "true", [1, 33]
185
+ def handle_kw(keyword, lineno_column)
186
+ raise Solid::SyntaxError, 'unknown Ripper sexp' unless KEYWORDS.has_key? keyword
187
+ KEYWORDS[keyword]
188
+ end
189
+
190
+ # # spam
191
+ # "spam", [1, 23]
192
+ def handle_ident(identifier, lineno_column)
193
+ ContextVariable.new identifier
194
+ end
195
+
196
+ # # Spam
197
+ # "Spam", [1, 23]
198
+ def handle_const(constant, lineno_column)
199
+ ContextVariable.new constant
200
+ end
201
+
202
+ # # 42
203
+ # "42", [1, 2]
204
+ def handle_int(int, lineno_column)
205
+ Literal.new int.to_i
206
+ end
207
+
208
+ # # 4.2
209
+ # "4.2", [1, 2]
210
+ def handle_float(float, lineno_column)
211
+ Literal.new float.to_f
212
+ end
213
+
214
+ # # foo:
215
+ # "foo:", [1, 2]
216
+ def handle_label(label, lineno_column)
217
+ Literal.new label[0..-2].to_sym
218
+ end
219
+
220
+ end
@@ -0,0 +1,88 @@
1
+ require 'ruby_parser'
2
+
3
+ class Solid::Parser::RubyParser < Solid::Parser
4
+
5
+ def self.parse(string)
6
+ new(string).parse
7
+ end
8
+
9
+ def initialize(expression)
10
+ @expression = expression
11
+ end
12
+
13
+ def parse
14
+ begin
15
+ @sexp = ::RubyParser.new.parse(@expression)
16
+ rescue ::RubyParser::SyntaxError => exc
17
+ raise Solid::SyntaxError.new(exc.message)
18
+ end
19
+ parse_one(@sexp)
20
+ end
21
+
22
+ def parse_one(expression)
23
+ type = expression.shift
24
+ handler = "handle_#{type}"
25
+ raise Solid::SyntaxError, "unknown expression type: #{type.inspect}" unless respond_to?(handler)
26
+ public_send handler, *expression
27
+ end
28
+
29
+ def handle_lit(literal)
30
+ case literal
31
+ when Range # see https://github.com/seattlerb/ruby_parser/issues/134
32
+ LiteralRange.new(Literal.new(literal.first), Literal.new(literal.last), literal.exclude_end?)
33
+ else
34
+ Literal.new(literal)
35
+ end
36
+ end
37
+
38
+ def handle_str(literal)
39
+ Literal.new(literal)
40
+ end
41
+
42
+ def handle_array(*array_values)
43
+ LiteralArray.new(array_values.map(&method(:parse_one)))
44
+ end
45
+
46
+ def handle_hash(*hash_keys_and_values)
47
+ LiteralHash.new(Hash[*hash_keys_and_values.map(&method(:parse_one))])
48
+ end
49
+
50
+ def handle_dot2(start_value, end_value)
51
+ LiteralRange.new(parse_one(start_value), parse_one(end_value), false)
52
+ end
53
+
54
+ def handle_dot3(start_value, end_value)
55
+ LiteralRange.new(parse_one(start_value), parse_one(end_value), true)
56
+ end
57
+
58
+ def handle_true
59
+ KEYWORDS['true']
60
+ end
61
+
62
+ def handle_false
63
+ KEYWORDS['false']
64
+ end
65
+
66
+ def handle_nil
67
+ KEYWORDS['nil']
68
+ end
69
+
70
+ def handle_const(const_name)
71
+ ContextVariable.new(const_name.to_s)
72
+ end
73
+
74
+ def handle_call(receiver, method_name, *arguments)
75
+ return ContextVariable.new(method_name.to_s) if receiver.nil?
76
+
77
+ MethodCall.new(parse_one(receiver), method_name.to_s, arguments.map(&method(:parse_one)))
78
+ end
79
+
80
+ def handle_and(left_expression, right_expression)
81
+ handle_call(left_expression, :'&&', right_expression)
82
+ end
83
+
84
+ def handle_or(left_expression, right_expression)
85
+ handle_call(left_expression, :'||', right_expression)
86
+ end
87
+
88
+ end
data/lib/solid/tag.rb ADDED
@@ -0,0 +1,11 @@
1
+ class Solid::Tag < Liquid::Tag
2
+
3
+ include Solid::Element
4
+
5
+ def render(context)
6
+ with_context(context) do
7
+ display(*arguments.interpolate(context).map(&Solid.method(:unproxify)))
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,24 @@
1
+ require 'forwardable'
2
+
3
+ class Solid::Template < Liquid::Template
4
+ extend Forwardable
5
+ include Solid::Iterable
6
+
7
+ class << self
8
+
9
+ def parse(source)
10
+ template = Solid::Template.new
11
+ template.parse(source)
12
+ template
13
+ end
14
+
15
+ end
16
+
17
+ def_delegators :root, :nodelist
18
+
19
+ # Avoid issues with ActiveSupport::Cache which freeze all objects passed to it like an ass
20
+ # And anyway once frozen Liquid::Templates are unable to render anything
21
+ def freeze
22
+ end
23
+
24
+ end