garterbelt 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/.document +5 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +16 -0
  4. data/Gemfile.lock +38 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.rdoc +21 -0
  7. data/Rakefile +46 -0
  8. data/TODO +3 -0
  9. data/VERSION +1 -0
  10. data/garterbelt.gemspec +165 -0
  11. data/lib/garterbelt.rb +23 -0
  12. data/lib/page.rb +46 -0
  13. data/lib/renderers/cache.rb +35 -0
  14. data/lib/renderers/closed_tag.rb +60 -0
  15. data/lib/renderers/comment.rb +14 -0
  16. data/lib/renderers/content_rendering.rb +41 -0
  17. data/lib/renderers/content_tag.rb +36 -0
  18. data/lib/renderers/doctype.rb +24 -0
  19. data/lib/renderers/renderer.rb +33 -0
  20. data/lib/renderers/text.rb +28 -0
  21. data/lib/renderers/xml.rb +9 -0
  22. data/lib/stocking.rb +11 -0
  23. data/lib/support/string.rb +165 -0
  24. data/lib/view.rb +341 -0
  25. data/spec/benchmark/templates/erector.rb +37 -0
  26. data/spec/benchmark/templates/garterbelt.rb +37 -0
  27. data/spec/benchmark/vs_erector.rb +53 -0
  28. data/spec/garterbelt_spec.rb +49 -0
  29. data/spec/integration/expectations/general_view.html +17 -0
  30. data/spec/integration/expectations/variables/view_with_user_and_params.html +23 -0
  31. data/spec/integration/expectations/variables/view_with_user_email.html +23 -0
  32. data/spec/integration/expectations/view_partial_nest.html +24 -0
  33. data/spec/integration/expectations/view_with_tags.html +19 -0
  34. data/spec/integration/templates/view_partial_nest.rb +22 -0
  35. data/spec/integration/templates/view_with_cache.rb +30 -0
  36. data/spec/integration/templates/view_with_partial.rb +36 -0
  37. data/spec/integration/templates/view_with_partial_2.rb +36 -0
  38. data/spec/integration/templates/view_with_tags.rb +26 -0
  39. data/spec/integration/templates/view_with_vars.rb +32 -0
  40. data/spec/integration/view_spec.rb +57 -0
  41. data/spec/page_spec.rb +99 -0
  42. data/spec/renderers/cache_spec.rb +85 -0
  43. data/spec/renderers/closed_tag_spec.rb +172 -0
  44. data/spec/renderers/comment_spec.rb +68 -0
  45. data/spec/renderers/content_tag_spec.rb +150 -0
  46. data/spec/renderers/doctype_spec.rb +46 -0
  47. data/spec/renderers/text_spec.rb +68 -0
  48. data/spec/spec_helper.rb +17 -0
  49. data/spec/support/mock_view.rb +14 -0
  50. data/spec/support/puters.rb +10 -0
  51. data/spec/view/view_basics_spec.rb +106 -0
  52. data/spec/view/view_caching_spec.rb +132 -0
  53. data/spec/view/view_partial_spec.rb +63 -0
  54. data/spec/view/view_rails_type_helpers.rb +148 -0
  55. data/spec/view/view_render_spec.rb +408 -0
  56. data/spec/view/view_variables_spec.rb +159 -0
  57. metadata +367 -0
@@ -0,0 +1,172 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Garterbelt::ClosedTag do
4
+ ClosedTag = Garterbelt::ClosedTag unless defined?(ClosedTag)
5
+
6
+ before do
7
+ @output = ''
8
+ @view = mock(:output => @output, :level => 2)
9
+ end
10
+
11
+ describe 'initialize' do
12
+ it 'requires a type' do
13
+ lambda{ ClosedTag.new({:view => @view}) }.should raise_error(ArgumentError, ":type required in initialization options")
14
+ end
15
+
16
+ it 'requires a view' do
17
+ lambda{ ClosedTag.new({:type => :input}) }.should raise_error(ArgumentError, ":view required in initialization options")
18
+ end
19
+
20
+ it 'store the type as an attribute' do
21
+ ClosedTag.new({:type => :input, :view => @view}).type.should == :input
22
+ end
23
+
24
+ it 'attributes should be empty by default' do
25
+ ClosedTag.new(:type => :input, :view => @view).attributes.should == {}
26
+ end
27
+
28
+ it 'sets the attributes' do
29
+ ClosedTag.new(:type => :input, :attributes => {:foo => :bar}, :view => @view).attributes.should == {:foo => :bar}
30
+ end
31
+
32
+ it 'extracts css_class into its own variable' do
33
+ ClosedTag.new(:type => :input, :attributes => {:class => :foo}, :view => @view).css_class.should == [:foo]
34
+ end
35
+ end
36
+
37
+ describe 'pooling' do
38
+ it 'include RuPol::Swimsuit' do
39
+ ClosedTag.ancestors.should include(RuPol::Swimsuit)
40
+ end
41
+
42
+ it 'has a really large max_pool_size' do
43
+ ClosedTag._pool.max_size.should == 10000
44
+ end
45
+ end
46
+
47
+ describe 'method chaining' do
48
+ before do
49
+ @tag = ClosedTag.new(:type => :input, :view => @view)
50
+ end
51
+
52
+ describe '#id' do
53
+ it 'adds an id attribute' do
54
+ @tag.id(:foo).attributes[:id].should == :foo
55
+ end
56
+
57
+ it 'raises an argument error if passed an array or something non-stringy' do
58
+ lambda{ @tag.id([:foo, :bar]) }.should raise_error(ArgumentError, "Id must be a String or Symbol")
59
+ end
60
+
61
+ it 'returns self' do
62
+ @tag.id(:foo).should === @tag
63
+ end
64
+ end
65
+
66
+ describe '#c' do
67
+ it 'adds the value to the css_class' do
68
+ @tag.c(:foo).css_class.should == [:foo]
69
+ end
70
+
71
+ it 'will not overwrite existing css classes' do
72
+ @tag.c(:foo).css_class.should == [:foo]
73
+ @tag.c(:bar).css_class.should == [:foo, :bar]
74
+ end
75
+
76
+ it 'takes any number of arguments' do
77
+ @tag.c(:foo, :bar).css_class.should == [:foo, :bar]
78
+ end
79
+
80
+ it 'returns self' do
81
+ @tag.c(:foo, :bar).should === @tag
82
+ end
83
+ end
84
+ end
85
+
86
+ describe 'view usage' do
87
+ before do
88
+ @tag = ClosedTag.new(:type => :input, :view => @view)
89
+ end
90
+
91
+ it 'uses its output' do
92
+ @tag.output.should == @output
93
+ end
94
+
95
+ it 'uses its level' do
96
+ @tag.level.should == 2
97
+ end
98
+ end
99
+
100
+
101
+ describe 'rendering' do
102
+ before do
103
+ @tag = ClosedTag.new(
104
+ :type => :input,
105
+ :attributes => {:class => :foo_bar, :thing => :thong},
106
+ :view => @view
107
+ )
108
+ end
109
+
110
+ it 'indent corresponding to the view level' do
111
+ @tag.indent.should == " "
112
+ @tag.stub(:level).and_return(1)
113
+ @tag.indent.should == " "
114
+ @tag.stub(:level).and_return(0)
115
+ @tag.indent.should == ""
116
+ end
117
+
118
+ describe '#rendered_attributes' do
119
+ it 'includes the css_class' do
120
+ @tag.rendered_attributes.should include "class=\"foo_bar\""
121
+ end
122
+
123
+ it 'multiple classes are separated by a space' do
124
+ @tag.c(:more_classy)
125
+ @tag.rendered_attributes.should include "class=\"foo_bar more_classy\""
126
+ end
127
+
128
+ it 'include other key/value pairs' do
129
+ @tag.rendered_attributes.should include "thing=\"thong\""
130
+ end
131
+
132
+ it 'does not include attributes with nil or false values' do
133
+ @tag.attributes[:checked] = false
134
+ @tag.attributes[:nily] = nil
135
+ rendered = @tag.rendered_attributes
136
+ rendered.should_not include "checked=\"\""
137
+ rendered.should_not include "nily=\"\""
138
+ end
139
+
140
+ it 'should subs out double quotes from attributes' do
141
+ @tag.attributes[:foo_title] = 'I am not "sure" if this will work'
142
+ @tag.rendered_attributes.should include "I am not 'sure' if this will work"
143
+ end
144
+ end
145
+
146
+ describe 'integration' do
147
+ before do
148
+ @str = @tag.render
149
+ end
150
+
151
+ it 'starts with the indent' do
152
+ @str.should match /^\W{4}</
153
+ end
154
+
155
+ it 'includes the full tag' do
156
+ @str.should match /<input[^>]*>/
157
+ end
158
+
159
+ it 'includes the attributes' do
160
+ @str.should match /<input#{@tag.rendered_attributes}>/
161
+ end
162
+
163
+ it 'ends with the closing tag and a line break' do
164
+ @str.should match /\n$/
165
+ end
166
+
167
+ it 'adds the string to the output' do
168
+ @output.should include @str
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,68 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Garterbelt::Comment do
4
+ before :all do
5
+ @view = MockView.new
6
+ end
7
+
8
+ describe 'basics' do
9
+ it 'is decends from Renderer' do
10
+ Garterbelt::Comment.ancestors.should include Garterbelt::Renderer
11
+ end
12
+
13
+ it 'has conent' do
14
+ comment = Garterbelt::Comment.new(:view => @view, :content => "Initializing ...")
15
+ comment.content.should == "Initializing ..."
16
+ comment.content = "foo"
17
+ comment.content.should == "foo"
18
+ end
19
+
20
+ it 'raises an error when initializing without content' do
21
+ lambda{ Garterbelt::Comment.new(:view => @view) }.should raise_error(
22
+ ArgumentError, ":content option required for Garterbelt::Comment initialization"
23
+ )
24
+ end
25
+
26
+ it 'has a smaller pool size' do
27
+ Garterbelt::Comment._pool.max_size.should == 1000
28
+ end
29
+ end
30
+
31
+ describe 'render' do
32
+ before do
33
+ @view = MockView.new
34
+ @comment = Garterbelt::Comment.new(:view => @view, :content => 'Render me')
35
+ end
36
+
37
+ it 'raises an error with block content' do
38
+ @comment.content = lambda { puts "foo" }
39
+ lambda{ @comment.render }.should raise_error(ArgumentError, "Garterbelt::Comment does not take block content")
40
+ end
41
+
42
+ it 'it adds the content to the output' do
43
+ @comment.render
44
+ @view.output.should include "Render me"
45
+ end
46
+
47
+ it 'builds the right header tag' do
48
+ @comment.render
49
+ @view.output.should match /<!-- Render me/
50
+ end
51
+
52
+ it 'builds the right footer tag' do
53
+ @comment.render
54
+ @view.output.should match /Render me -->/
55
+ end
56
+
57
+ it 'indents to the view level' do
58
+ @comment.render
59
+ @view.output.should match /^\W{4}<!-- Render me/
60
+ end
61
+
62
+ it 'does not escape the content' do
63
+ @comment.content = "<div>foo</div>"
64
+ @comment.render
65
+ @view.output.should include "<div>foo</div>"
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,150 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Garterbelt::ContentTag do
4
+ ContentTag = Garterbelt::ContentTag unless defined?(ContentTag)
5
+
6
+ before do
7
+ @view = MockView.new
8
+ @output = @view.output
9
+ @params = {:type => :p, :attributes => {:class => 'classy'}, :view => @view}
10
+ @tag = ContentTag.new(@params)
11
+ end
12
+
13
+ describe "basics" do
14
+ it 'takes a content option' do
15
+ ContentTag.new(@params.merge(:content => 'My great content')).content.should == "My great content"
16
+ end
17
+
18
+ it 'takes a block as content' do
19
+ ContentTag.new(@params) do
20
+ @output << "This is block content"
21
+ end.content.class.should == Proc
22
+ end
23
+
24
+ it 'will override option content in favor of block content' do
25
+ ContentTag.new(@params.merge(:content => 'not the block')) do
26
+ @output << "This is block content"
27
+ end.content.class.should == Proc
28
+ end
29
+
30
+ it 'inherits a really large max_pool_size' do
31
+ ContentTag._pool.max_size.should == 10000
32
+ end
33
+ end
34
+
35
+ describe 'chaining' do
36
+ describe 'id' do
37
+ it 'takes a block and sets it to content' do
38
+ @tag.id(:foo) do
39
+ @output << "This is block content"
40
+ end
41
+ @tag.content.class.should == Proc
42
+ end
43
+
44
+ it 'returns self' do
45
+ @tag.id(:foo).should === @tag
46
+ end
47
+ end
48
+
49
+ describe 'c' do
50
+ it 'takes a block and sets it to content' do
51
+ @tag.c(:foo) do
52
+ @output << "This is block content"
53
+ end
54
+ @tag.content.class.should == Proc
55
+ end
56
+
57
+ it 'returns self' do
58
+ @tag.c(:foo).should === @tag
59
+ end
60
+ end
61
+ end
62
+
63
+ describe 'rendering' do
64
+ describe 'tags' do
65
+ before do
66
+ @tag.content = 'My string content'
67
+ @tag.render
68
+ end
69
+
70
+ it 'indents the beginning tag correctly' do
71
+ @output.should match /^ <p/
72
+ end
73
+
74
+ it 'renders the beginning tag correctly' do
75
+ @output.should include "<p class=\"classy\">"
76
+ end
77
+
78
+ it 'indents the ending tag correctly' do
79
+ @output.should match /^ <\/p/
80
+ end
81
+
82
+ it 'renders the ending tag correctly' do
83
+ @output.should include "</p>"
84
+ end
85
+ end
86
+
87
+ describe 'content' do
88
+ describe 'none' do
89
+ it 'works' do
90
+ @tag.content.should be_nil
91
+ @tag.render
92
+ @output.should match " <p class=\"classy\">\n </p>"
93
+ end
94
+ end
95
+
96
+ describe 'string' do
97
+ before do
98
+ @tag.content = "My string content"
99
+ end
100
+
101
+ it 'makes a Text object' do
102
+ Garterbelt::Text.should_receive(:new).and_return('text')
103
+ @tag.render
104
+ end
105
+ end
106
+
107
+ describe 'block' do
108
+ describe 'writing directly to output' do
109
+ before do
110
+ @tag.id(:foo) do
111
+ @output << "Going directly to the source"
112
+ end
113
+ @tag.render
114
+ end
115
+
116
+ it 'adds the content' do
117
+ @output.should include "Going directly to the source"
118
+ end
119
+
120
+ it 'does not indent' do
121
+ @output.should match /^Going/
122
+ end
123
+
124
+ it 'should put the content between the tags' do
125
+ @output.should match /<p.*\nGoing.*<\/p/
126
+ end
127
+ end
128
+
129
+ describe 'adding to the tag buffer' do
130
+ before do
131
+ @b = Garterbelt::ClosedTag.new(:view => @view, :type => :hr, :attributes => {:class => :linear})
132
+ @tag.id(:foo) do
133
+ @view.buffer << @b
134
+ end
135
+ end
136
+
137
+ it 'should add the tag to the buffer' do
138
+ @tag.render
139
+ @view.buffer.should include @b
140
+ end
141
+
142
+ it 'calls render buffer on the view' do
143
+ @view.should_receive(:render_buffer)
144
+ @tag.render
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,46 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Garterbelt::Doctype do
4
+ before :all do
5
+ @view = MockView.new
6
+ end
7
+
8
+ describe 'basics' do
9
+ it 'is decends from ClosedTag' do
10
+ Garterbelt::Doctype.ancestors.should include Garterbelt::ClosedTag
11
+ end
12
+
13
+ it 'has a smaller pool size' do
14
+ Garterbelt::Doctype._pool.max_size.should == 1000
15
+ end
16
+ end
17
+
18
+ describe 'render' do
19
+ before do
20
+ @view = MockView.new
21
+ @doctype = Garterbelt::Doctype.new(:view => @view, :type => :transitional)
22
+ end
23
+
24
+ it 'builds the right tag for type = :transitional' do
25
+ tag = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
26
+ @doctype.render.should include tag
27
+ end
28
+
29
+ it 'builds the right tag for type = :strict' do
30
+ tag = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
31
+ @doctype.type = :strict
32
+ @doctype.render.should include tag
33
+ end
34
+
35
+ it 'builds the right tag for type = :html5' do
36
+ tag = '<!DOCTYPE html>'
37
+ @doctype.type = :html5
38
+ @doctype.render.should include tag
39
+ end
40
+
41
+ it 'indents to the view level' do
42
+ @doctype.render
43
+ @view.output.should match /^\W{4}<!DOCTYPE/
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,68 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Garterbelt::Text do
4
+ before :all do
5
+ @view = MockView.new
6
+ end
7
+
8
+ describe 'basics' do
9
+ it 'is decends from Renderer' do
10
+ Garterbelt::Text.ancestors.should include Garterbelt::Renderer
11
+ end
12
+
13
+ it 'has conent' do
14
+ text = Garterbelt::Text.new(:view => @view, :content => "Initializing ...")
15
+ text.content.should == "Initializing ..."
16
+ text.content = "foo"
17
+ text.content.should == "foo"
18
+ end
19
+
20
+ it 'raises an error when initializing without content' do
21
+ lambda{ Garterbelt::Text.new(:view => @view) }.should raise_error(
22
+ ArgumentError, ":content option required for Garterbelt::Text initialization"
23
+ )
24
+ end
25
+
26
+ it 'inherits its pool size' do
27
+ Garterbelt::Text._pool.max_size.should == 10000
28
+ end
29
+ end
30
+
31
+ describe 'render' do
32
+ before do
33
+ @view = MockView.new
34
+ @text = Garterbelt::Text.new(:view => @view, :content => 'Render me')
35
+ end
36
+
37
+ it 'raises an error with block content' do
38
+ @text.content = lambda { puts "foo" }
39
+ lambda{ @text.render }.should raise_error(ArgumentError, "Garterbelt::Text does not take block content")
40
+ end
41
+
42
+ it 'it adds the content to the output' do
43
+ @text.render
44
+ @view.output.should include "Render me"
45
+ end
46
+
47
+ it 'indents to the view level' do
48
+ @text.render
49
+ @view.output.should match /^\W{4}Render me\n$/
50
+ end
51
+
52
+ describe 'escaping' do
53
+ it 'escapes if view is set to escape' do
54
+ str = "<a href='/foo.com'>Foo it!</a>"
55
+ text = Garterbelt::Text.new(:view => @view, :content => str)
56
+ text.render.should_not include str
57
+ text.render.should include ERB::Util.html_escape(str)
58
+ end
59
+
60
+ it 'does not escape if the view is set to not escape' do
61
+ str = "<a href='/foo.com'>Foo it!</a>"
62
+ @view.escape = false
63
+ text = Garterbelt::Text.new(:view => @view, :content => str)
64
+ text.render.should include str
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,17 @@
1
+ require 'rubygems'
2
+
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
5
+
6
+ require 'rspec'
7
+ require 'hashie'
8
+ require 'garterbelt'
9
+
10
+ # Requires supporting files with custom matchers and macros, etc,
11
+ # in ./support/ and its subdirectories.
12
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
13
+ Dir["#{File.dirname(__FILE__)}/integration/templates/**/*.rb"].each {|f| require f}
14
+
15
+ RSpec.configure do |config|
16
+ include PutSpec
17
+ end
@@ -0,0 +1,14 @@
1
+ class MockView
2
+ attr_accessor :output, :buffer, :level, :escape, :cache_store
3
+
4
+ def initialize
5
+ self.buffer = []
6
+ self.output = ""
7
+ self.level ||= 2
8
+ self.escape = true
9
+ self.cache_store = Moneta::Memory.new
10
+ end
11
+
12
+ def render_buffer
13
+ end
14
+ end
@@ -0,0 +1,10 @@
1
+ require 'cgi'
2
+ module PutSpec
3
+ def putspec message
4
+ puts CGI.escapeHTML("#{message.inspect}") if message
5
+ end
6
+
7
+ def hr
8
+ puts "<hr>"
9
+ end
10
+ end
@@ -0,0 +1,106 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Garterbelt::View do
4
+ class BasicView < Garterbelt::View
5
+ def content
6
+ end
7
+
8
+ def alt_content
9
+ end
10
+ end
11
+
12
+ before do
13
+ @view = BasicView.new
14
+ end
15
+
16
+ describe 'pooling' do
17
+ it 'includes the swimsuit' do
18
+ BasicView.ancestors.should include( RuPol::Swimsuit )
19
+ end
20
+ end
21
+
22
+ describe 'attributes' do
23
+ it 'has a tag buffer' do
24
+ @view.buffer.should == []
25
+ @tag = Garterbelt::ContentTag.new(:view => @view, :type => :hr)
26
+ @view.buffer << @tag
27
+ @view.buffer.should == [@tag]
28
+ end
29
+
30
+ describe 'output' do
31
+ it 'has an output' do
32
+ @view.output.should == ""
33
+ end
34
+
35
+ it 'its output is that of the curator if the curator is not self' do
36
+ BasicView.new(:curator => @view).output.should === @view.output
37
+ end
38
+ end
39
+
40
+ describe 'escape' do
41
+ it 'has escape set to true by default' do
42
+ @view.escape.should == true
43
+ end
44
+
45
+ it 'can be set' do
46
+ @view.escape = false
47
+ @view.escape.should == false
48
+ BasicView.new(:escape => false).escape.should == false
49
+ end
50
+ end
51
+
52
+ describe 'level' do
53
+ it 'is 0 by default' do
54
+ @view.level.should == 0
55
+ end
56
+
57
+ it 'can be set via initialization' do
58
+ BasicView.new(:level => 42).level.should == 42
59
+ end
60
+ end
61
+
62
+ describe 'setting the curator: view responsible for displaying the rendered content' do
63
+ before do
64
+ @view.level = 42
65
+ @view.output = "foo"
66
+ @view.buffer = ["bar"]
67
+ @view.escape = false
68
+ @child = BasicView.new(:curator => @view)
69
+ end
70
+
71
+ it 'is self by default' do
72
+ @view.curator.should == @view
73
+ end
74
+
75
+ it 'can be set' do
76
+ @view.curator = BasicView.new
77
+ @view.curator.should_not == @view
78
+ end
79
+
80
+ it 'can be intialized in' do
81
+ BasicView.new(:curator => @view).curator.should == @view
82
+ end
83
+
84
+ describe 'resets other attributes' do
85
+ it 'sets the output to the curator\'s' do
86
+ @child.output.should === @view.output
87
+ end
88
+
89
+ it 'sets the level to the curator\'s' do
90
+ @child.level.should == @view.level
91
+ @child.level.should == 42
92
+ end
93
+
94
+ it 'sets the buffer to the curator\'s' do
95
+ @child.buffer.should === @view.buffer
96
+ end
97
+
98
+ it 'sets the escape to the curator\'s' do
99
+ @child.escape.should == @view.escape
100
+ end
101
+ end
102
+ end
103
+
104
+ end
105
+ end
106
+