locomotivecms-solid 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.travis.yml +10 -0
- data/Gemfile +6 -0
- data/LICENSE +20 -0
- data/README.md +152 -0
- data/Rakefile +7 -0
- data/lib/locomotivecms-solid.rb +2 -0
- data/lib/solid.rb +48 -0
- data/lib/solid/arguments.rb +26 -0
- data/lib/solid/block.rb +13 -0
- data/lib/solid/conditional_block.rb +35 -0
- data/lib/solid/context_error.rb +2 -0
- data/lib/solid/default_security_rules.rb +24 -0
- data/lib/solid/element.rb +51 -0
- data/lib/solid/engine.rb +4 -0
- data/lib/solid/extensions.rb +17 -0
- data/lib/solid/iterable.rb +18 -0
- data/lib/solid/liquid_extensions.rb +87 -0
- data/lib/solid/liquid_extensions/assign_tag.rb +21 -0
- data/lib/solid/liquid_extensions/for_tag.rb +102 -0
- data/lib/solid/liquid_extensions/if_tag.rb +44 -0
- data/lib/solid/liquid_extensions/unless_tag.rb +13 -0
- data/lib/solid/liquid_extensions/variable.rb +34 -0
- data/lib/solid/method_whitelist.rb +56 -0
- data/lib/solid/model_drop.rb +119 -0
- data/lib/solid/parser.rb +108 -0
- data/lib/solid/parser/ripper.rb +220 -0
- data/lib/solid/parser/ruby_parser.rb +88 -0
- data/lib/solid/tag.rb +11 -0
- data/lib/solid/template.rb +24 -0
- data/lib/solid/version.rb +3 -0
- data/locomotivecms-solid.gemspec +26 -0
- data/spec/solid/arguments_spec.rb +314 -0
- data/spec/solid/block_spec.rb +39 -0
- data/spec/solid/conditional_block_spec.rb +39 -0
- data/spec/solid/default_security_rules_spec.rb +180 -0
- data/spec/solid/element_examples.rb +67 -0
- data/spec/solid/liquid_extensions/assign_tag_spec.rb +27 -0
- data/spec/solid/liquid_extensions/for_tag_spec.rb +48 -0
- data/spec/solid/liquid_extensions/if_tag_spec.rb +64 -0
- data/spec/solid/liquid_extensions/unless_tag_spec.rb +54 -0
- data/spec/solid/liquid_extensions/variable_spec.rb +25 -0
- data/spec/solid/model_drop_spec.rb +26 -0
- data/spec/solid/parser/ripper_spec.rb +14 -0
- data/spec/solid/parser/ruby_parser_spec.rb +7 -0
- data/spec/solid/tag_spec.rb +26 -0
- data/spec/solid/template_spec.rb +37 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/class_highjacker_examples.rb +33 -0
- data/spec/support/method_whitelist_matchers.rb +17 -0
- data/spec/support/parser_examples.rb +261 -0
- data/spec/support/tag_highjacker_examples.rb +33 -0
- metadata +204 -0
data/lib/solid/parser.rb
ADDED
@@ -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,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
|