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,3 @@
1
+ module Solid
2
+ VERSION = '0.2.2'
3
+ end
@@ -0,0 +1,26 @@
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
+ s.add_development_dependency "rspec"
20
+ s.add_development_dependency "rake"
21
+ s.add_development_dependency "i18n"
22
+ s.add_development_dependency "ruby_parser", "~> 3.2"
23
+ s.add_development_dependency "activesupport", "~> 3"
24
+
25
+ s.add_runtime_dependency "locomotivecms-liquid", "~> 2.6.0"
26
+ end
@@ -0,0 +1,314 @@
1
+ require 'spec_helper'
2
+
3
+ describe Solid::Arguments do
4
+
5
+ class DummyDrop < Liquid::Drop
6
+
7
+ def before_method(name)
8
+ "dummy #{name}"
9
+ end
10
+
11
+ end
12
+
13
+ def parse(string, context={})
14
+ Solid::Arguments.parse(string).interpolate(Liquid::Context.new(context))
15
+ end
16
+
17
+ context 'with no arguments' do
18
+
19
+ it "parses as an empty array" do
20
+ parse('').should be == []
21
+ end
22
+
23
+ end
24
+
25
+ context 'with a single argument' do
26
+
27
+ context 'of type string' do
28
+
29
+ it 'can parse an empty string' do
30
+ parse("''").should be == ['']
31
+ end
32
+
33
+ it 'can parse a constant' do
34
+ parse("FooBar", {'FooBar' => 42}).should be == [42]
35
+ end
36
+
37
+ it 'can parse a simple string (between simple quotes)' do
38
+ parse("'foobar'").should be == ['foobar']
39
+ end
40
+
41
+ it 'can parse a simple string (between double quotes)' do
42
+ parse('"foobar"').should be == ['foobar']
43
+ end
44
+
45
+ it 'should not consider this string as a context var' do
46
+ parse('"foobar"', {'foobar' => 'plop'}).should_not == ['plop']
47
+ end
48
+
49
+ it 'should not be disturbed by a string containing a comma' do
50
+ parse(%{"foo,bar", 'egg,spam'}).should be == ['foo,bar', 'egg,spam']
51
+ end
52
+
53
+ it 'should not be disturbed by a string containing a simple quote' do
54
+ parse('"foo\'bar"').should be == ["foo'bar"]
55
+ end
56
+
57
+ it 'should not be disturbed by a string containing a double quote' do
58
+ parse("'foo\"bar'").should be == ['foo"bar']
59
+ end
60
+
61
+ pending('not yet implemented') do
62
+ it 'should work for a string containing interpolation' do
63
+ parse('"1#{foo}3"', {'foo' => 2}).should be == ['123']
64
+ end
65
+ end
66
+
67
+ end
68
+
69
+ context 'of type integer' do
70
+
71
+ it 'should work' do
72
+ parse('42').should be == [42]
73
+ end
74
+
75
+ end
76
+
77
+ context 'of type float' do
78
+
79
+ it 'should work' do
80
+ parse('4.2').should be == [4.2]
81
+ end
82
+
83
+ end
84
+
85
+ context 'of type boolean' do
86
+
87
+ it 'should work with `true`' do
88
+ parse('true').should be == [true]
89
+ end
90
+
91
+ it 'should work with `false`' do
92
+ parse('false').should be == [false]
93
+ end
94
+
95
+ end
96
+
97
+ context 'of type Regexp' do
98
+
99
+ it 'should work for simple cases' do
100
+ parse('/bb|[^b]{2}/').should be == [/bb|[^b]{2}/]
101
+ end
102
+
103
+ it 'should work for a regexp containing interpolation' do
104
+ pending('not yet implemented')
105
+ parse('/#{mystring}|[^b]{2}/', {'mystring' => 'bb'}).should be == [/bb|[^b]{2}/]
106
+ end
107
+
108
+ end
109
+
110
+ context 'of type Range' do
111
+
112
+ it 'should work for integer ranges' do
113
+ parse('1..10').should be == [1..10]
114
+ end
115
+
116
+ it 'should work for integer exclusive ranges' do
117
+ parse('1...10').should be == [1...10]
118
+ end
119
+
120
+ it 'should work for float ranges' do
121
+ parse('1.0..10.0').should be == [1.0..10.0]
122
+ end
123
+
124
+ it 'should work with context variables' do
125
+ parse('a..b', {'a' => 1, 'b' => 10}).should be == [1..10]
126
+ end
127
+
128
+ end
129
+
130
+ context 'binary operators' do
131
+
132
+ it 'should permit additions' do
133
+ parse('1 + 2').should be == [3]
134
+ end
135
+
136
+ it 'should permit multiplications' do
137
+ parse('2 * 3').should be == [6]
138
+ end
139
+
140
+ it 'should permit to use builtins boolean operators' do
141
+ parse('true && false').should be == [false]
142
+ parse('false || true').should be == [true]
143
+ end
144
+
145
+ end
146
+
147
+ context 'unary operators' do
148
+
149
+ it 'should allow to use "!"' do
150
+ parse('!true').should be == [false]
151
+ parse('!false').should be == [true]
152
+ end
153
+
154
+ end
155
+
156
+ context 'of type "context var"' do
157
+
158
+ it 'should work' do
159
+ parse('myvar', {'myvar' => 'myvalue'}).should be == ['myvalue']
160
+ end
161
+
162
+ it 'can call methods without arguments' do
163
+ parse('myvar.length', {'myvar' => ' myvalue '}).should be == [9]
164
+ end
165
+
166
+ it 'can call methods without arguments on immediate values' do
167
+ parse('"foobar".length').should be == [6]
168
+ end
169
+
170
+ it 'can call methods without arguments but parentheses on immediate values' do
171
+ parse('"foobar".length()').should be == [6]
172
+ end
173
+
174
+ it 'can call a method with arguments' do
175
+ parse('myvar.split(",", 2)', {'myvar' => 'foo,bar'}).should be == [%w(foo bar)]
176
+ end
177
+
178
+ it 'can call a method with context var arguments' do
179
+ parse('myvar.split(myseparator, 2)', {'myvar' => 'foo,bar', 'myseparator' => ','}).should be == [%w(foo bar)]
180
+ end
181
+
182
+ it 'can evaluate context var deeply unclosed in collections' do
183
+ parse('[{1 => [{2 => myvar}]}]', {'myvar' => 'myvalue'}).first.should be == [{1 => [{2 => 'myvalue'}]}]
184
+ end
185
+
186
+ it 'can call methods chain without arguments' do
187
+ parse('myvar.strip.length', {'myvar' => ' myvalue '}).should be == [7]
188
+ end
189
+
190
+ it 'can call predicate methods' do
191
+ parse('myvar.empty?', {'myvar' => ' myvalue '}).should be == [false]
192
+ end
193
+
194
+ it 'can get a hash value' do
195
+ parse('myvar.mykey', {'myvar' => {'mykey' => 'myvalue'}}).should be == ['myvalue']
196
+ end
197
+
198
+ it 'can fallback on Liquid::Drop#before_method' do
199
+ parse('myvar.mymethod', {'myvar' => DummyDrop.new}).should be == ['dummy mymethod']
200
+ end
201
+
202
+ it 'should manage errors'
203
+
204
+ end
205
+
206
+ context 'of type hash' do
207
+
208
+ it 'should be able to parse unclosed hashes' do
209
+ parse('permissions: ""').should be == [{permissions: ''}]
210
+ end
211
+
212
+ end
213
+
214
+ context 'of type "named parameter"' do
215
+
216
+ it 'should be able to parse a string' do
217
+ parse('foo:"bar"').should be == [{:foo => 'bar'}]
218
+ end
219
+
220
+ it 'should be able to parse an int' do
221
+ parse('foo:42').should be == [{:foo => 42}]
222
+ end
223
+
224
+ it 'should be able to parse a context var' do
225
+ parse('foo:bar', {'bar' => 'baz'}).should be == [{:foo => 'baz'}]
226
+ end
227
+
228
+ it "should not be disturbed by a comma into a named string" do
229
+ parse('foo:"bar,baz"').should be == [{:foo => 'bar,baz'}]
230
+ end
231
+
232
+ it "should support a mix of types" do
233
+ parse('foo:"bar",bar:42, baz:true, egg:spam', {'spam' => 'egg'}).should be == [{
234
+ foo: 'bar',
235
+ bar: 42,
236
+ baz: true,
237
+ egg: 'egg',
238
+ }]
239
+ end
240
+
241
+ it "should not be disturbed by a dot into the key" do
242
+ parse(':"foo.bar" => "bar"').should be == [{:'foo.bar' => 'bar'}]
243
+ end
244
+
245
+ end
246
+
247
+ end
248
+
249
+ context 'security !!!' do
250
+
251
+ it 'should not allow to call unsecure methods' do
252
+ parse('42.send("`", "echo foo")').should be == [nil]
253
+ end
254
+
255
+ it 'should not allow to call unsecure methods' do
256
+ parse('42.__send__("`", "echo foo")').should be == [nil]
257
+ end
258
+
259
+ it "should raise a Solid::SyntaxError on unknown constructs" do
260
+ expect {
261
+ parse('{}\[]')
262
+ }.to raise_error(Solid::SyntaxError)
263
+ end
264
+
265
+ it "should use #to_liquid" do
266
+ drop = Object.new
267
+ def drop.to_liquid
268
+ 'liquid'
269
+ end
270
+ parse('drop', 'drop' => drop).should be == ['liquid']
271
+ end
272
+
273
+ it "should use #context= if available" do
274
+ drop = Object.new
275
+ class << drop
276
+ def to_liquid
277
+ self
278
+ end
279
+ def to_s
280
+ @alias
281
+ end
282
+ def context=(context)
283
+ @alias = context['alias']
284
+ end
285
+ end
286
+ parse('drop.to_s', 'drop' => drop, 'alias' => 'liquid').should be == ['liquid']
287
+ end
288
+
289
+ end
290
+
291
+ context 'with useless round brackets' do
292
+
293
+ it 'should still work' do
294
+ parse('(42)').should be == [42]
295
+ parse('(((((((42)))))))').should be == [42]
296
+ end
297
+
298
+ end
299
+
300
+ context 'with multiple arguments' do
301
+
302
+ it 'should return 3 arguments and an option hash' do
303
+ args = parse('1, "2", myvar, myopt:false', {'myvar' => 4.2})
304
+ args.should be == [1, '2', 4.2, {:myopt => false}]
305
+ end
306
+
307
+ it 'should be tolerant about whitespace around commas and colons' do
308
+ args = parse(" 1\t, '2' ,myvar, myopt: false", {'myvar' => 4.2})
309
+ args.should be == [1, '2', 4.2, {:myopt => false}]
310
+ end
311
+
312
+ end
313
+
314
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ class DummyBlock < Solid::Block
4
+
5
+ def display(condition)
6
+ if condition
7
+ yield
8
+ else
9
+ 'not_yielded'
10
+ end
11
+ end
12
+
13
+ end
14
+
15
+ describe Solid::Block do
16
+
17
+ it_behaves_like "a Solid element"
18
+
19
+ describe '#display' do
20
+
21
+ let(:tokens) { ["dummy", "{% enddummy %}", "outside"] }
22
+
23
+ subject{ DummyBlock.new('dummy', 'condition', tokens) }
24
+
25
+ it 'yielding should render the block content' do
26
+ subject.render(Liquid::Context.new('condition' => true)).should be == 'dummy'
27
+ end
28
+
29
+ it 'should only render until the {% endblock %} tag' do
30
+ subject.render(Liquid::Context.new('condition' => true)).should_not include('outside')
31
+ end
32
+
33
+ it 'should not render its content if it do not yield' do
34
+ subject.render(Liquid::Context.new('condition' => false)).should_not include('dummy')
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ class IfPresent < Solid::ConditionalBlock
4
+
5
+ def display(string)
6
+ yield(!string.strip.empty?)
7
+ end
8
+
9
+ end
10
+
11
+ describe Solid::ConditionalBlock do
12
+
13
+ it_behaves_like "a Solid element"
14
+
15
+ describe '#display' do
16
+
17
+ let(:tokens) { ["present", "{% else %}", "blank", "{% endifpresent %}"] }
18
+
19
+ subject{ IfPresent.new('ifpresent', 'mystring', tokens) }
20
+
21
+ it 'yielding true should render the main block' do
22
+ context = Liquid::Context.new('mystring' => 'blah')
23
+ subject.render(context).should be == 'present'
24
+ end
25
+
26
+ it 'yielding false should render the `else` block' do
27
+ context = Liquid::Context.new('mystring' => '')
28
+ subject.render(context).should be == 'blank'
29
+ end
30
+
31
+ it 'yielding false without a `else` block does not render anything' do
32
+ context = Liquid::Context.new('mystring' => '')
33
+ subject = IfPresent.new('ifpresent', 'mystring', ['present', '{% endifpresent %}'])
34
+ subject.render(context).should be_nil
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,180 @@
1
+ require 'spec_helper'
2
+ require 'active_support/core_ext'
3
+
4
+ describe Solid, 'default security rules' do
5
+
6
+ shared_examples_for 'a ruby object' do
7
+ it_should_safely_respond_to :nil?, :==, :!=, :!, :!~, :blank?, :present?, :in?, :to_json
8
+ it_should_not_safely_respond_to :const_get, :const_set,
9
+ :instance_variable_get, :instance_variable_set, :instance_variable_defined?,
10
+ :send, :__send__, :public_send,
11
+ :__id__, :object_id,
12
+ :class, :singleton_class, :trust, :taint, :untaint, :untrust,
13
+ :clone, :dup, :initialize_dup, :initialize_clone, :freeze,
14
+ :methods, :singleton_methods, :protected_methods, :private_methods,
15
+ :method, :public_method, :define_singleton_method, :extend,
16
+ :eval, :instance_eval, :instance_exec, :exec, :`, :system, :test,
17
+ :global_variables, :local_variables,
18
+ :gets, :readline, :readlines, :sleep,
19
+ :at_exit!, :exit, :fork, :spawn, :trap, :exit!, :syscall
20
+ end
21
+
22
+ shared_examples_for 'a ruby module' do
23
+ it_should_behave_like 'a ruby object'
24
+ it_should_not_safely_respond_to :ancestors
25
+ end
26
+
27
+ shared_examples_for 'a ruby class' do
28
+ it_should_behave_like 'a ruby module'
29
+ it_should_not_safely_respond_to :new, :allocate, :superclass
30
+ end
31
+
32
+ shared_examples_for 'a boolean' do
33
+ it_should_behave_like 'a ruby object'
34
+ it_should_safely_respond_to :false?, :true?
35
+ end
36
+
37
+ shared_examples_for 'an enumerable' do
38
+ it_should_safely_respond_to :sort, :length, :size
39
+ end
40
+
41
+ shared_examples_for 'a comparable' do
42
+ it_should_safely_respond_to :<, :<=, :==, :>, :>=, :between?
43
+ end
44
+
45
+ shared_examples_for 'a numeric' do
46
+ it_should_behave_like 'a ruby object', 'a comparable'
47
+ it_should_safely_respond_to :%, :*, :**, :+, :-, :-@, :/, :<=>, :===, :to_s, :abs,
48
+ :second, :seconds, :minute, :minutes, :hour, :hours, :day, :days, :week, :weeks,
49
+ :bytes, :kilobytes, :megabytes, :gigabytes, :terabytes, :petabytes, :exabytes
50
+ end
51
+
52
+ shared_examples_for 'an integer' do
53
+ it_should_behave_like 'a numeric'
54
+ it_should_safely_respond_to :div, :divmod, :even?, :odd?, :to_f,
55
+ :month, :months, :year, :years
56
+ end
57
+
58
+ describe 'nil' do
59
+ subject { nil }
60
+
61
+ it_should_behave_like 'a ruby object'
62
+ end
63
+
64
+ describe true do
65
+ subject { true }
66
+
67
+ it_should_behave_like 'a ruby object'
68
+ end
69
+
70
+ describe false do
71
+ subject { false }
72
+
73
+ it_should_behave_like 'a ruby object'
74
+ end
75
+
76
+ describe 'Basic object instance' do
77
+ let(:basic_class) { Class.new(Object) }
78
+ subject { basic_class.new }
79
+
80
+ it_should_behave_like 'a ruby object'
81
+ end
82
+
83
+ describe 'Array instances' do
84
+ subject { [] }
85
+
86
+ it_should_behave_like 'a ruby object', 'an enumerable'
87
+ it_should_safely_respond_to :[], :[]=, :first, :last, :join, :reverse, :uniq, :include?, :empty?,
88
+ :to_sentence, :in_groups_of, :in_groups
89
+ end
90
+
91
+ describe 'Array class' do
92
+ subject { Array }
93
+
94
+ it_should_behave_like 'a ruby class'
95
+ it_should_safely_respond_to :wrap
96
+ end
97
+
98
+ describe 'Hash instances' do
99
+ subject { {} }
100
+
101
+ it_should_behave_like 'a ruby object', 'an enumerable'
102
+ it_should_safely_respond_to :[], :[]=, :has_key?, :has_value?, :empty?, :except, :slice
103
+ end
104
+
105
+ describe 'Range instances' do
106
+ subject { 1..10 }
107
+
108
+ it_should_behave_like 'a ruby object', 'an enumerable'
109
+ it_should_safely_respond_to :first, :last, :begin, :end, :max, :min, :cover?, :include?, :member?
110
+ end
111
+
112
+ describe 'Regexp instances' do
113
+ subject { /bb|[^b]{2}/ }
114
+
115
+ it_should_behave_like 'a ruby object'
116
+ it_should_safely_respond_to :==, :===, :=~, :match
117
+ end
118
+
119
+ describe 'Bignum instances' do
120
+ subject { 2 ** 123 }
121
+
122
+ it { should be_a Bignum }
123
+ it_should_behave_like 'an integer'
124
+ end
125
+
126
+ describe 'Fixnum instances' do
127
+ subject { 4 }
128
+
129
+ it_should_behave_like 'an integer'
130
+ it_should_safely_respond_to :multiple_of?
131
+ end
132
+
133
+ describe 'Float instances' do
134
+ subject { 4.2 }
135
+
136
+ it_should_behave_like 'a numeric'
137
+ end
138
+
139
+ describe 'Time class' do
140
+ subject { Time }
141
+
142
+ it_should_behave_like 'a ruby class'
143
+ it_should_safely_respond_to :at, :now
144
+ end
145
+
146
+ describe 'Time instances' do
147
+ subject { Time.now }
148
+
149
+ it_should_safely_respond_to :to_i, :to_f, :<=>,
150
+ :localtime, :gmtime, :utc, :getlocal, :getgm, :getutc,
151
+ :ctime, :asctime, :to_s, :inspect, :to_a, :+, :-, :round,
152
+ :sec, :min, :hour, :mday, :day, :mon, :month, :year, :wday, :yday,
153
+ :isdst, :dst?, :zone, :gmtoff, :gmt_offset, :utc_offset, :utc?, :gmt?,
154
+ :sunday?, :monday?, :tuesday?, :wednesday?, :thursday?, :friday?, :saturday?,
155
+ :tv_sec, :tv_usec, :usec, :tv_nsec, :nsec, :subsec, :strftime,
156
+ :to_time, :to_date, :to_datetime
157
+ end
158
+
159
+ describe 'String instances' do
160
+ subject { 'string' }
161
+
162
+ it_should_behave_like 'a ruby object', 'a comparable', 'an enumerable'
163
+ it_should_safely_respond_to :gsub, :strip, :chop, :chomp, :start_with?, :end_with?,
164
+ :[], :length, :size, :empty?, :=~, :split, :upcase, :downcase, :capitalize, :squeeze, :tr,
165
+ :exclude?, :truncate
166
+ end
167
+
168
+ describe 'Module instances' do
169
+ subject { Module.new }
170
+
171
+ it_should_behave_like 'a ruby module'
172
+ end
173
+
174
+ describe 'Class instances' do
175
+ subject { Class.new }
176
+
177
+ it_should_behave_like 'a ruby class'
178
+ end
179
+
180
+ end