locomotivecms_solid 0.2.1

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