locomotivecms-solid 0.2.2

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