locomotivecms_solid 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. data/.gitignore +4 -0
  2. data/.rspec +1 -0
  3. data/.rvmrc +1 -0
  4. data/.travis.yml +9 -0
  5. data/Gemfile +4 -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/arguments.rb +28 -0
  11. data/lib/solid/block.rb +13 -0
  12. data/lib/solid/conditional_block.rb +35 -0
  13. data/lib/solid/context_error.rb +2 -0
  14. data/lib/solid/default_security_rules.rb +24 -0
  15. data/lib/solid/element.rb +51 -0
  16. data/lib/solid/engine.rb +4 -0
  17. data/lib/solid/extensions.rb +17 -0
  18. data/lib/solid/iterable.rb +18 -0
  19. data/lib/solid/liquid_extensions/assign_tag.rb +21 -0
  20. data/lib/solid/liquid_extensions/for_tag.rb +102 -0
  21. data/lib/solid/liquid_extensions/if_tag.rb +44 -0
  22. data/lib/solid/liquid_extensions/unless_tag.rb +13 -0
  23. data/lib/solid/liquid_extensions/variable.rb +34 -0
  24. data/lib/solid/liquid_extensions.rb +87 -0
  25. data/lib/solid/method_whitelist.rb +56 -0
  26. data/lib/solid/model_drop.rb +119 -0
  27. data/lib/solid/parser.rb +265 -0
  28. data/lib/solid/tag.rb +11 -0
  29. data/lib/solid/template.rb +24 -0
  30. data/lib/solid/version.rb +3 -0
  31. data/lib/solid.rb +48 -0
  32. data/locomotivecms_solid.gemspec +25 -0
  33. data/spec/solid/arguments_spec.rb +310 -0
  34. data/spec/solid/block_spec.rb +39 -0
  35. data/spec/solid/conditional_block_spec.rb +39 -0
  36. data/spec/solid/default_security_rules_spec.rb +180 -0
  37. data/spec/solid/element_examples.rb +67 -0
  38. data/spec/solid/liquid_extensions/assign_tag_spec.rb +27 -0
  39. data/spec/solid/liquid_extensions/for_tag_spec.rb +48 -0
  40. data/spec/solid/liquid_extensions/if_tag_spec.rb +64 -0
  41. data/spec/solid/liquid_extensions/unless_tag_spec.rb +54 -0
  42. data/spec/solid/liquid_extensions/variable_spec.rb +25 -0
  43. data/spec/solid/model_drop_spec.rb +26 -0
  44. data/spec/solid/tag_spec.rb +26 -0
  45. data/spec/solid/template_spec.rb +37 -0
  46. data/spec/spec_helper.rb +8 -0
  47. data/spec/support/class_highjacker_examples.rb +33 -0
  48. data/spec/support/method_whitelist_matchers.rb +17 -0
  49. data/spec/support/tag_highjacker_examples.rb +33 -0
  50. metadata +201 -0
@@ -0,0 +1,87 @@
1
+ module Solid
2
+
3
+ class << self
4
+
5
+ def extend_liquid!
6
+ LiquidExtensions.load!
7
+ end
8
+
9
+ end
10
+
11
+ module LiquidExtensions
12
+
13
+ module ClassHighjacker
14
+
15
+ def load!
16
+ original_class = Liquid.send(:remove_const, demodulized_name)
17
+ original_classes[demodulized_name] = original_class unless original_classes.has_key?(demodulized_name) # avoid loosing reference to original class
18
+ Liquid.const_set(demodulized_name, self)
19
+ end
20
+
21
+ def unload!
22
+ if original_class = original_classes[demodulized_name]
23
+ Liquid.send(:remove_const, demodulized_name)
24
+ Liquid.const_set(demodulized_name, original_class)
25
+ end
26
+ end
27
+
28
+ def demodulized_name
29
+ @demodulized_name ||= self.name.split('::').last
30
+ end
31
+
32
+ protected
33
+ def original_classes
34
+ @@original_classes ||= {}
35
+ end
36
+
37
+ end
38
+
39
+ module TagHighjacker
40
+
41
+ def load!
42
+ original_tag = Liquid::Template.tags[tag_name.to_s]
43
+ original_tags[tag_name] = original_tag unless original_tags.has_key?(tag_name) # avoid loosing reference to original class
44
+ Liquid::Template.register_tag(tag_name, self)
45
+ end
46
+
47
+ def unload!
48
+ Liquid::Template.register_tag(tag_name, original_tags[tag_name])
49
+ end
50
+
51
+ def tag_name(name=nil)
52
+ @tag_name = name unless name.nil?
53
+ @tag_name
54
+ end
55
+
56
+ protected
57
+ def original_tags
58
+ @@original_tags ||= {}
59
+ end
60
+
61
+ end
62
+
63
+ BASE_PATH = File.join(File.expand_path(File.dirname(__FILE__)), 'liquid_extensions')
64
+
65
+ # FIXME: Keep the original ForTag from being modified.
66
+ %w(if_tag unless_tag assign_tag variable).each do |mod|
67
+ require File.join(BASE_PATH, mod)
68
+ end
69
+
70
+ # FIXME: Keep the original ForTag from being modified.
71
+ ALL = [IfTag, UnlessTag, AssignTag, Variable]
72
+
73
+ class << self
74
+
75
+ def load!
76
+ ALL.each(&:load!)
77
+ end
78
+
79
+ def unload!
80
+ ALL.each(&:unload!)
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+
87
+ end
@@ -0,0 +1,56 @@
1
+ module Solid
2
+ module MethodWhitelist
3
+ extend self
4
+
5
+ METHODS_WHITELIST = {}
6
+ METHODS_BLACKLIST = {}
7
+
8
+ class << self
9
+
10
+ def allow(rules)
11
+ rules.each do |owner, method_names|
12
+ list = METHODS_WHITELIST[owner] ||= Set.new
13
+ [method_names].flatten.each do |method_name|
14
+ list.add(method_name.to_sym)
15
+ end
16
+ end
17
+ self
18
+ end
19
+
20
+ def deny(rules)
21
+ rules.each do |owner, method_names|
22
+ list = METHODS_BLACKLIST[owner] ||= Set.new
23
+ [method_names].flatten.each do |method_name|
24
+ list.add(method_name.to_sym)
25
+ end
26
+ end
27
+ self
28
+ end
29
+
30
+ end
31
+
32
+ def safely_respond_to?(object, method)
33
+ return false unless object.respond_to?(method, false)
34
+ method = object.method(method)
35
+ (!inherited?(object, method) || whitelisted?(method)) && !blacklisted?(method)
36
+ end
37
+ module_function :safely_respond_to?
38
+
39
+ private
40
+
41
+ def whitelisted?(method)
42
+ METHODS_WHITELIST.has_key?(method.owner) && METHODS_WHITELIST[method.owner].include?(method.name)
43
+ end
44
+
45
+ def blacklisted?(method)
46
+ METHODS_BLACKLIST.has_key?(method.owner) && METHODS_BLACKLIST[method.owner].include?(method.name)
47
+ end
48
+
49
+ def inherited?(object, method)
50
+ method.owner != object.class && !object.methods(false).include?(method.name)
51
+ end
52
+
53
+ end
54
+ end
55
+
56
+ require File.join(File.expand_path(File.dirname(__FILE__)), 'default_security_rules')
@@ -0,0 +1,119 @@
1
+ class Solid::ModelDrop < Liquid::Drop
2
+
3
+ module ModelExtension
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+
8
+ def to_drop
9
+ "#{self.name}Drop".constantize.new(current_scope || self)
10
+ end
11
+
12
+ end
13
+
14
+ end
15
+
16
+ class_attribute :dynamic_methods
17
+
18
+ class << self
19
+
20
+ def model(model_name=nil)
21
+ if model_name
22
+ @model_name = model_name
23
+ else
24
+ @model_name ||= self.name.gsub(/Drop$/, '')
25
+ end
26
+ end
27
+
28
+ def model_class
29
+ @model_class ||= self.model.to_s.camelize.constantize
30
+ end
31
+
32
+ def immutable_method(method_name)
33
+ self.class_eval <<-END_EVAL, __FILE__, __LINE__ + 1
34
+ def #{method_name}_with_immutation(*args, &block)
35
+ self.dup.tap do |clone|
36
+ clone.#{method_name}_without_immutation(*args, &block)
37
+ end
38
+ end
39
+ END_EVAL
40
+ self.alias_method_chain method_name, :immutation
41
+ end
42
+
43
+ def respond(options={})
44
+ raise ArgumentError.new(":to option should be a Regexp") unless options[:to].is_a?(Regexp)
45
+ raise ArgumentError.new(":with option is mandatory") unless options[:with].present?
46
+ self.dynamic_methods ||= []
47
+ self.dynamic_methods += [[options[:to], options[:with]]]
48
+ end
49
+
50
+ def allow_scopes(*scopes)
51
+ @allowed_scopes = scopes
52
+ scopes.each do |scope_name|
53
+ self.class_eval <<-END_EVAL, __FILE__, __LINE__ + 1
54
+ def #{scope_name}(*args)
55
+ @scope = scope.public_send(:#{scope_name}, *args)
56
+ end
57
+ END_EVAL
58
+ self.immutable_method(scope_name)
59
+ end
60
+ end
61
+
62
+ end
63
+
64
+ delegate :model_class, :to => 'self.class'
65
+
66
+ respond :to => /limited_to_(\d+)/, :with => :limit_to
67
+
68
+ def initialize(base_scope=nil, context=nil)
69
+ @scope = base_scope
70
+ @context ||= context
71
+ end
72
+
73
+ def all
74
+ self
75
+ end
76
+
77
+ def each(&block)
78
+ scope.each(&block)
79
+ end
80
+
81
+ def before_method(method_name, *args)
82
+ self.class.dynamic_methods.each do |pattern, method|
83
+ if match_data = pattern.match(method_name)
84
+ return self.send(method, *match_data[1..-1])
85
+ end
86
+ end
87
+ raise NoMethodError.new("undefined method `#{method_name}' for #{self.inspect}")
88
+ end
89
+
90
+ delegate :to_a, to: :each
91
+ delegate *(Array.public_instance_methods - self.public_instance_methods), to: :to_a
92
+
93
+ protected
94
+
95
+ def limit_to(size)
96
+ @scope = scope.limit(size.to_i)
97
+ end
98
+ immutable_method :limit_to
99
+
100
+ def scope
101
+ @scope ||= default_scope
102
+ end
103
+
104
+ def default_scope
105
+ model_class
106
+ end
107
+
108
+ def context
109
+ @context
110
+ end
111
+
112
+ private
113
+
114
+ if Rails.env.test? # Just for cleaner and simpler specs
115
+ def method_missing(name, *args, &block)
116
+ before_method(name.to_s)
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,265 @@
1
+ require 'set'
2
+ require 'ripper'
3
+
4
+ class Solid::Parser
5
+
6
+ class ContextVariable < Struct.new(:name)
7
+ def evaluate(context)
8
+ Solid.to_liquid(context[name], context)
9
+ end
10
+ end
11
+
12
+ class Literal < Struct.new(:value)
13
+ def evaluate(context)
14
+ Solid.to_liquid(value, context)
15
+ end
16
+ end
17
+
18
+ class LiteralArray < Literal
19
+ def evaluate(context)
20
+ value.map{ |v| v.evaluate(context) }
21
+ end
22
+ end
23
+
24
+ class LiteralRange < Struct.new(:start_value, :end_value, :exclusive)
25
+ def evaluate(context)
26
+ Range.new(start_value.evaluate(context), end_value.evaluate(context), exclusive)
27
+ end
28
+ end
29
+
30
+ class LiteralHash < Literal
31
+ def evaluate(context)
32
+ Hash[value.map{ |k, v| [k.evaluate(context), v.evaluate(context)] }]
33
+ end
34
+ end
35
+
36
+ class MethodCall < Struct.new(:receiver, :name, :arguments)
37
+ include Solid::MethodWhitelist
38
+ BUILTIN_HANDLERS = {
39
+ :'&&' => ->(left, right) { left && right },
40
+ :'||' => ->(left, right) { left || right }
41
+ }
42
+
43
+ def evaluate(context)
44
+ Solid.to_liquid(pluck(receiver.evaluate(context), name, *arguments.map {|arg| arg.evaluate(context) }), context)
45
+ end
46
+
47
+ protected
48
+
49
+ def pluck(object, method, *args)
50
+ if BUILTIN_HANDLERS.has_key?(method)
51
+ BUILTIN_HANDLERS[method].call(object, *args)
52
+ elsif safely_respond_to?(object, method)
53
+ object.public_send(method, *args)
54
+ elsif object.respond_to?(:[]) && args.empty?
55
+ object[method]
56
+ elsif object.respond_to?(:before_method)
57
+ object.before_method(method, *args)
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ class << self
64
+
65
+ def parse(string)
66
+ new(string).parse
67
+ end
68
+
69
+ end
70
+
71
+ def initialize(string)
72
+ @string = string
73
+ @sexp = nil
74
+ end
75
+
76
+ def parse
77
+ @sexp = Ripper.sexp(@string)
78
+ dive_in or raise 'Ripper changed?'
79
+ parse_one(@sexp)
80
+ end
81
+
82
+ # Looks for a structure like
83
+ # [:program, [[:array, [#stuff#]]]] or
84
+ # [:program, [[:array, nil]]]
85
+ def dive_in
86
+ @sexp = @sexp[1]
87
+ @sexp = @sexp.first
88
+ end
89
+
90
+ def parse_one(argument)
91
+ type = argument.shift
92
+ handler = "handle_#{type.to_s.sub('@', '')}"
93
+ raise Solid::SyntaxError, "unknown Ripper type: #{type.inspect}" unless respond_to?(handler)
94
+ public_send handler, *argument
95
+ end
96
+
97
+ # # spam
98
+ # [:@ident, "spam", [1, 33]] or
99
+ # # true
100
+ # [:@kw, "true", [1, 23]]
101
+ def handle_var_ref(var_ref)
102
+ parse_one(var_ref)
103
+ end
104
+
105
+ # # foo: 42
106
+ # [[:assoc_new, [:@label, "foo:", [1, 1]], [:@int, "42", [1, 5]]]]
107
+ def handle_bare_assoc_hash(assoc_hash)
108
+ LiteralHash.new assoc_hash.map {|(_, *key_value)| key_value.map(&method(:parse_one)) }
109
+ end
110
+
111
+ # # {foo: 42}
112
+ # [:assoclist_from_args, [[:assoc_new, [:@label, "foo:", [1, 2]], [:@int, "42", [1, 7]]]]]
113
+ def handle_hash(hash)
114
+ handle_bare_assoc_hash(hash.last)
115
+ end
116
+
117
+ # # myvar.length
118
+ #
119
+ # [:var_ref, [:@ident, "myvar", [1, 1]]]
120
+ # :"."
121
+ # [:@ident, "length", [1, 7]]
122
+ def handle_call(receiver_sexp, method_call, method_sexp)
123
+ receiver = parse_one(receiver_sexp)
124
+ method = method_sexp[1]
125
+ MethodCall.new receiver, method, []
126
+ end
127
+
128
+ # # myvar
129
+ #
130
+ # since 1.9.3
131
+ # [:vcall, [:@ident, "myvar", [1, 0]]]
132
+ def handle_vcall(expression)
133
+ parse_one(expression)
134
+ end
135
+
136
+ # # myvar.split(',', 2)
137
+ #
138
+ # [:call, [:var_ref, [:@ident, "myvar", [1, 1]]], :".", [:@ident, "split", [1, 7]]]
139
+ # [:arg_paren, [:args_add_block, [
140
+ # [:string_literal, [:string_content, [:@tstring_content, ",", [1, 14]]]],
141
+ # [:@int, "2", [1, 18]]
142
+ # ], false]]
143
+ def handle_method_add_arg(call_sexp, args_sexp)
144
+ method_call = parse_one(call_sexp)
145
+ method_call.arguments = method_call_args(args_sexp)
146
+ method_call
147
+ end
148
+
149
+ # # args list: (',', 2)
150
+ # [:arg_paren, [:args_add_block, [
151
+ # [:string_literal, [:string_content, [:@tstring_content, ",", [1, 14]]]],
152
+ # [:@int, "2", [1, 18]]
153
+ # ], false]]
154
+ #
155
+ # 1 args list: ()
156
+ # [:arg_paren, nil]
157
+ def method_call_args(args_sexp)
158
+ return [] if args_sexp[1].nil?
159
+ args_sexp = args_sexp.last[1]
160
+ args_sexp.map(&method(:parse_one))
161
+ end
162
+
163
+ # # !true
164
+ # [:!, [:var_ref, [:@kw, "true", [1, 1]]]]
165
+ def handle_unary(operator, operand)
166
+ MethodCall.new(parse_one(operand), operator, [])
167
+ end
168
+
169
+ # # 1 + 2
170
+ # [:@int, "1", [1, 0]], :*, [:@int, "2", [1, 4]]
171
+ def handle_binary(left_operand, operator, right_operand)
172
+ receiver = parse_one(left_operand)
173
+ MethodCall.new(receiver, operator, [parse_one(right_operand)])
174
+ end
175
+
176
+ # # [1]
177
+ # [[:@int, "1", [1, 1]]
178
+ def handle_array(array)
179
+ LiteralArray.new((array || []).map(&method(:parse_one)))
180
+ end
181
+
182
+ # # (1)
183
+ # [[:@int, "42", [1, 2]]]
184
+ def handle_paren(content)
185
+ parse_one(content.first)
186
+ end
187
+
188
+ # # 1..10
189
+ # [[:@int, "1", [1, 0]], [:@int, "10", [1, 4]]]
190
+ def handle_dot2(start_value, end_value)
191
+ LiteralRange.new(parse_one(start_value), parse_one(end_value), false)
192
+ end
193
+
194
+ # # 1...10
195
+ # [[:@int, "1", [1, 0]], [:@int, "10", [1, 4]]]
196
+ def handle_dot3(start_value, end_value)
197
+ LiteralRange.new(parse_one(start_value), parse_one(end_value), true)
198
+ end
199
+
200
+ # # 'mystring'
201
+ # [:string_content, [:@tstring_content, "mystring", [1, 14]]]
202
+ # TODO: handle string interpolation
203
+ def handle_string_literal(string_content)
204
+ Literal.new(parse_one(string_content))
205
+ end
206
+
207
+ def handle_string_content(*parts)
208
+ parts.map(&method(:parse_one)).join
209
+ end
210
+
211
+ # [:@tstring_content, "mystring", [1, 14]]
212
+ def handle_tstring_content(string_content, lineno_column)
213
+ string_content
214
+ end
215
+
216
+ # # /bb|[^b]{2}/
217
+ # [[:@tstring_content, "bb|[^b]{2}", [1, 2]]].first.first
218
+ # TODO: handle regexp interpolation
219
+ def handle_regexp_literal(regexp_literal, lineno_column)
220
+ Literal.new Regexp.new(regexp_literal.first[1])
221
+ end
222
+
223
+ KEYWORDS = {
224
+ 'true' => Literal.new(true),
225
+ 'false' => Literal.new(false),
226
+ 'nil' => Literal.new(nil),
227
+ }
228
+ # # true
229
+ # "true", [1, 33]
230
+ def handle_kw(keyword, lineno_column)
231
+ raise Solid::SyntaxError, 'unknown Ripper sexp' unless KEYWORDS.has_key? keyword
232
+ KEYWORDS[keyword]
233
+ end
234
+
235
+ # # spam
236
+ # "spam", [1, 23]
237
+ def handle_ident(identifier, lineno_column)
238
+ ContextVariable.new identifier
239
+ end
240
+
241
+ # # Spam
242
+ # "Spam", [1, 23]
243
+ def handle_const(constant, lineno_column)
244
+ ContextVariable.new constant
245
+ end
246
+
247
+ # # 42
248
+ # "42", [1, 2]
249
+ def handle_int(int, lineno_column)
250
+ Literal.new int.to_i
251
+ end
252
+
253
+ # # 4.2
254
+ # "4.2", [1, 2]
255
+ def handle_float(float, lineno_column)
256
+ Literal.new float.to_f
257
+ end
258
+
259
+ # # foo:
260
+ # "foo:", [1, 2]
261
+ def handle_label(label, lineno_column)
262
+ Literal.new label[0..-2].to_sym
263
+ end
264
+
265
+ 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
@@ -0,0 +1,3 @@
1
+ module Solid
2
+ VERSION = '0.2.1'
3
+ end
data/lib/solid.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'liquid'
2
+
3
+ module Solid
4
+ BASE_PATH = File.join(File.expand_path(File.dirname(__FILE__)), 'solid')
5
+
6
+ require File.join(BASE_PATH, 'extensions')
7
+
8
+ autoload :Argument, File.join(BASE_PATH, 'argument')
9
+ autoload :Arguments, File.join(BASE_PATH, 'arguments')
10
+ autoload :Block, File.join(BASE_PATH, 'block')
11
+ autoload :ConditionalBlock, File.join(BASE_PATH, 'conditional_block')
12
+ autoload :ContextError, File.join(BASE_PATH, 'context_error')
13
+ autoload :Element, File.join(BASE_PATH, 'element')
14
+ autoload :Iterable, File.join(BASE_PATH, 'iterable')
15
+ autoload :MethodWhitelist, File.join(BASE_PATH, 'method_whitelist')
16
+ autoload :Parser, File.join(BASE_PATH, 'parser')
17
+ autoload :Tag, File.join(BASE_PATH, 'tag')
18
+ autoload :Template, File.join(BASE_PATH, 'template')
19
+ autoload :VERSION, File.join(BASE_PATH, 'version')
20
+
21
+ if defined?(Rails) # Rails only features
22
+ autoload :ModelDrop, File.join(BASE_PATH, 'model_drop')
23
+ require File.join(BASE_PATH, 'engine')
24
+ end
25
+
26
+ require File.join(BASE_PATH, 'liquid_extensions')
27
+
28
+ class << self
29
+
30
+ def unproxify(object)
31
+ class_name = object.class.name
32
+ if class_name && class_name.end_with?('::LiquidDropClass')
33
+ return object.instance_variable_get('@object')
34
+ end
35
+ object
36
+ end
37
+
38
+ def to_liquid(object, context)
39
+ object = object.to_liquid
40
+ object.context = context if object.respond_to?(:context=)
41
+ object
42
+ end
43
+
44
+ end
45
+
46
+ SyntaxError = Class.new(Liquid::SyntaxError)
47
+
48
+ end
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "solid/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "locomotivecms_solid"
7
+ s.version = Solid::VERSION
8
+ s.authors = ["Jean Boussier", "Yannick François", "Didier Lafforgue"]
9
+ s.email = ["jean.boussier@tigerlilyapps.com", "yannick@tigerlilyapps.com", "didier.lafforgue@gmail.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Helpers for easily creating custom Liquid tags and block}
12
+ s.description = %q{The Solid gem from the TigerLily team but modified to work with LocomotiveCMS}
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ["lib"]
18
+
19
+ # specify any dependencies here; for example:
20
+ s.add_development_dependency "rspec"
21
+ s.add_development_dependency "rake"
22
+ s.add_development_dependency "i18n"
23
+ s.add_development_dependency "activesupport", "~> 3"
24
+ s.add_runtime_dependency "locomotive_liquid", '~> 2.4.2'
25
+ end