reactive-ruby 0.7.3

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 (69) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +30 -0
  3. data/Gemfile +3 -0
  4. data/Gemfile.lock +53 -0
  5. data/LICENSE +19 -0
  6. data/README.md +303 -0
  7. data/config.ru +15 -0
  8. data/example/examples/Gemfile +7 -0
  9. data/example/examples/Gemfile.lock +45 -0
  10. data/example/examples/config.ru +44 -0
  11. data/example/examples/hello.js.rb +43 -0
  12. data/example/react-tutorial/Gemfile +7 -0
  13. data/example/react-tutorial/Gemfile.lock +49 -0
  14. data/example/react-tutorial/README.md +8 -0
  15. data/example/react-tutorial/_comments.json +14 -0
  16. data/example/react-tutorial/config.ru +63 -0
  17. data/example/react-tutorial/example.js.rb +290 -0
  18. data/example/react-tutorial/public/base.css +62 -0
  19. data/example/todos/Gemfile +11 -0
  20. data/example/todos/Gemfile.lock +84 -0
  21. data/example/todos/README.md +37 -0
  22. data/example/todos/Rakefile +8 -0
  23. data/example/todos/app/application.rb +22 -0
  24. data/example/todos/app/components/app.react.rb +61 -0
  25. data/example/todos/app/components/footer.react.rb +31 -0
  26. data/example/todos/app/components/todo_item.react.rb +46 -0
  27. data/example/todos/app/components/todo_list.react.rb +25 -0
  28. data/example/todos/app/models/todo.rb +19 -0
  29. data/example/todos/config.ru +14 -0
  30. data/example/todos/index.html.haml +16 -0
  31. data/example/todos/spec/todo_spec.rb +28 -0
  32. data/example/todos/vendor/base.css +410 -0
  33. data/example/todos/vendor/bg.png +0 -0
  34. data/example/todos/vendor/jquery.js +4 -0
  35. data/lib/rails-helpers/react_component.rb +32 -0
  36. data/lib/reactive-ruby.rb +23 -0
  37. data/lib/reactive-ruby/api.rb +177 -0
  38. data/lib/reactive-ruby/callbacks.rb +35 -0
  39. data/lib/reactive-ruby/component.rb +411 -0
  40. data/lib/reactive-ruby/element.rb +87 -0
  41. data/lib/reactive-ruby/event.rb +76 -0
  42. data/lib/reactive-ruby/ext/hash.rb +9 -0
  43. data/lib/reactive-ruby/ext/string.rb +8 -0
  44. data/lib/reactive-ruby/isomorphic_helpers.rb +223 -0
  45. data/lib/reactive-ruby/observable.rb +33 -0
  46. data/lib/reactive-ruby/rendering_context.rb +91 -0
  47. data/lib/reactive-ruby/serializers.rb +15 -0
  48. data/lib/reactive-ruby/state.rb +90 -0
  49. data/lib/reactive-ruby/top_level.rb +53 -0
  50. data/lib/reactive-ruby/validator.rb +83 -0
  51. data/lib/reactive-ruby/version.rb +3 -0
  52. data/logo1.png +0 -0
  53. data/logo2.png +0 -0
  54. data/logo3.png +0 -0
  55. data/reactive-ruby.gemspec +25 -0
  56. data/spec/callbacks_spec.rb +107 -0
  57. data/spec/component_spec.rb +597 -0
  58. data/spec/element_spec.rb +60 -0
  59. data/spec/event_spec.rb +22 -0
  60. data/spec/react_spec.rb +209 -0
  61. data/spec/reactjs/index.html.erb +11 -0
  62. data/spec/spec_helper.rb +29 -0
  63. data/spec/tutorial/tutorial_spec.rb +37 -0
  64. data/spec/validator_spec.rb +79 -0
  65. data/vendor/active_support/core_ext/array/extract_options.rb +29 -0
  66. data/vendor/active_support/core_ext/class/attribute.rb +127 -0
  67. data/vendor/active_support/core_ext/kernel/singleton_class.rb +13 -0
  68. data/vendor/active_support/core_ext/module/remove_method.rb +11 -0
  69. metadata +205 -0
@@ -0,0 +1,45 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ opal-react (0.1.1)
5
+ opal
6
+ opal-activesupport
7
+
8
+ GEM
9
+ remote: http://rubygems.org/
10
+ specs:
11
+ hike (1.2.3)
12
+ opal (0.8.0)
13
+ hike (~> 1.2)
14
+ sourcemap (~> 0.1.0)
15
+ sprockets (~> 3.1)
16
+ tilt (>= 1.4)
17
+ opal-activesupport (0.1.0)
18
+ opal (>= 0.5.0, < 1.0.0)
19
+ opal-jquery (0.4.0)
20
+ opal (>= 0.7.0, < 0.9.0)
21
+ rack (1.6.4)
22
+ rack-protection (1.5.3)
23
+ rack
24
+ react-source (0.13.3)
25
+ sinatra (1.4.6)
26
+ rack (~> 1.4)
27
+ rack-protection (~> 1.4)
28
+ tilt (>= 1.3, < 3)
29
+ sourcemap (0.1.1)
30
+ sprockets (3.2.0)
31
+ rack (~> 1.0)
32
+ tilt (2.0.1)
33
+
34
+ PLATFORMS
35
+ ruby
36
+
37
+ DEPENDENCIES
38
+ opal
39
+ opal-jquery
40
+ opal-react!
41
+ react-source
42
+ sinatra
43
+
44
+ BUNDLED WITH
45
+ 1.10.2
@@ -0,0 +1,44 @@
1
+ # config.ru
2
+ require 'bundler'
3
+ Bundler.require
4
+
5
+ require "react/source"
6
+
7
+ Opal::Processor.source_map_enabled = true
8
+
9
+ opal = Opal::Server.new {|s|
10
+ s.append_path './'
11
+ s.append_path File.dirname(::React::Source.bundled_path_for("react-with-addons.js"))
12
+ s.main = 'example'
13
+ s.debug = true
14
+ }
15
+
16
+ map opal.source_maps.prefix do
17
+ run opal.source_maps
18
+ end rescue nil
19
+
20
+ map '/assets' do
21
+ run opal.sprockets
22
+ end
23
+
24
+ get '/example/:example' do
25
+ example = params[:example]
26
+ <<-HTML
27
+ <!doctype html>
28
+ <html>
29
+ <head>
30
+ <title>Example: #{example}.rb</title>
31
+ <script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
32
+ <script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
33
+ <script src="/assets/react-with-addons.min.js"></script>
34
+ <script src="/assets/#{example}.js"></script>
35
+ <script>#{Opal::Processor.load_asset_code(opal.sprockets, example+".js")}</script>
36
+ </head>
37
+ <body>
38
+ <div id="content"></div>
39
+ </body>
40
+ </html>
41
+ HTML
42
+ end
43
+
44
+ run Sinatra::Application
@@ -0,0 +1,43 @@
1
+ require 'opal'
2
+ require 'opal-react'
3
+
4
+ class HelloMessage
5
+
6
+ include React::Component # will create a new component named HelloMessage
7
+
8
+ MSG = {great: 'Cool!', bad: 'Cheer up!'}
9
+
10
+ optional_param :mood
11
+ required_param :name
12
+ define_state :foo, "Default greeting"
13
+
14
+ before_mount do
15
+ foo! "#{name}: #{MSG[mood]}" if mood # change the state of foo using foo!, read the state using foo
16
+ end
17
+
18
+ after_mount :log # notice the two forms of callback
19
+
20
+ def log
21
+ puts "mounted!"
22
+ end
23
+
24
+ def render # render method MUST return just one component
25
+ div do # basic dsl syntax component_name(options) { ...children... }
26
+ span { "#{foo} #{name}!" } # all html5 components are defined with lower case text
27
+ end
28
+ end
29
+
30
+ end
31
+
32
+ class App
33
+ include React::Component
34
+
35
+ def render
36
+ HelloMessage name: 'John', mood: :great # new components are accessed via the class name
37
+ end
38
+ end
39
+
40
+ # later we will talk about nicer ways to do this: For now wait till doc is loaded
41
+ # then tell React to create an "App" and render it into the document body.
42
+
43
+ `window.onload = #{lambda {React.render(React.create_element(App), `document.body`)}}`
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'opal-react', :path => '../..'
4
+ gem 'opal-browser'
5
+ gem 'sinatra'
6
+ gem 'opal-jquery'
7
+ gem 'react-source'
@@ -0,0 +1,49 @@
1
+ PATH
2
+ remote: ../..
3
+ specs:
4
+ opal-react (0.2.1)
5
+ opal
6
+ opal-activesupport
7
+
8
+ GEM
9
+ remote: https://rubygems.org/
10
+ specs:
11
+ hike (1.2.3)
12
+ opal (0.8.0)
13
+ hike (~> 1.2)
14
+ sourcemap (~> 0.1.0)
15
+ sprockets (~> 3.1)
16
+ tilt (>= 1.4)
17
+ opal-activesupport (0.1.0)
18
+ opal (>= 0.5.0, < 1.0.0)
19
+ opal-browser (0.1.0.beta1)
20
+ opal (>= 0.5.5)
21
+ paggio
22
+ opal-jquery (0.4.0)
23
+ opal (>= 0.7.0, < 0.9.0)
24
+ paggio (0.2.4)
25
+ rack (1.6.4)
26
+ rack-protection (1.5.3)
27
+ rack
28
+ react-source (0.13.3)
29
+ sinatra (1.4.6)
30
+ rack (~> 1.4)
31
+ rack-protection (~> 1.4)
32
+ tilt (>= 1.3, < 3)
33
+ sourcemap (0.1.1)
34
+ sprockets (3.2.0)
35
+ rack (~> 1.0)
36
+ tilt (2.0.1)
37
+
38
+ PLATFORMS
39
+ ruby
40
+
41
+ DEPENDENCIES
42
+ opal-browser
43
+ opal-jquery
44
+ opal-react!
45
+ react-source
46
+ sinatra
47
+
48
+ BUNDLED WITH
49
+ 1.10.2
@@ -0,0 +1,8 @@
1
+ # React Tutorial
2
+
3
+ This is a rewrite of original comment box example using React.rb from the [React tutorial](http://facebook.github.io/react/docs/tutorial.html).
4
+
5
+ ## To use
6
+
7
+ 1. Make sure you use Bundler, then `bundle exec rackup`
8
+ 2. And visit <http://localhost:9292/>. Try opening multiple tabs!
@@ -0,0 +1,14 @@
1
+ [
2
+ {
3
+ "author": "Pete Hunt",
4
+ "text": "Hey there!"
5
+ },
6
+ {
7
+ "author": "Mitch",
8
+ "text": "i ***am*** saying something!"
9
+ },
10
+ {
11
+ "author": "Jan",
12
+ "text": "well what are you saying *dear*"
13
+ }
14
+ ]
@@ -0,0 +1,63 @@
1
+ # config.ru
2
+ require 'bundler'
3
+ Bundler.require
4
+
5
+ require "react/source"
6
+
7
+ Opal::Processor.source_map_enabled = true
8
+
9
+ opal = Opal::Server.new {|s|
10
+ s.append_path './'
11
+ s.append_path File.dirname(::React::Source.bundled_path_for("react-with-addons.js"))
12
+ s.main = 'example'
13
+ s.debug = true
14
+ }
15
+
16
+ map opal.source_maps.prefix do
17
+ run opal.source_maps
18
+ end rescue nil
19
+
20
+ map '/assets' do
21
+ run opal.sprockets
22
+ end
23
+
24
+ get '/comments.json' do
25
+ comments = JSON.parse(open("./_comments.json").read)
26
+ JSON.generate(comments)
27
+ end
28
+
29
+ get '/comments.js' do
30
+ content_type "application/javascript"
31
+ comments = JSON.parse(open("./_comments.json").read)
32
+ "window.initial_comments = #{JSON.generate(comments)}"
33
+ end
34
+
35
+ post "/comments.json" do
36
+ comments = JSON.parse(open("./_comments.json").read)
37
+ comments << JSON.parse(request.body.read)
38
+ File.write('./_comments.json', JSON.pretty_generate(comments, :indent => ' '))
39
+ JSON.generate(comments)
40
+ end
41
+
42
+ get '/' do
43
+ <<-HTML
44
+ <!doctype html>
45
+ <html>
46
+ <head>
47
+ <title>Hello React</title>
48
+ <link rel="stylesheet" href="base.css" />
49
+ <script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
50
+ <script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script>
51
+ <script src="/assets/react-with-addons.js"></script>
52
+ <script src="/assets/example.js"></script>
53
+ <script src="/comments.js"></script>
54
+ <script>#{Opal::Processor.load_asset_code(opal.sprockets, "example.js")}</script>
55
+ </head>
56
+ <body>
57
+ <div id="content"></div>
58
+ </body>
59
+ </html>
60
+ HTML
61
+ end
62
+
63
+ run Sinatra::Application
@@ -0,0 +1,290 @@
1
+ require 'opal'
2
+ require 'browser' # gives us wrappers on javascript methods such as setTimer and setInterval
3
+ require 'opal-jquery' # gives us a nice wrapper on jQuery which we will use mainly for HTTP calls
4
+ require "json" # json conversions
5
+ require 'opal-react' # and the whole reason we are gathered here today!
6
+
7
+ Document.ready? do # Document.ready? is a opal-jquery method. The block will run when doc is loaded
8
+
9
+ # render an instance of the CommentBox component at the '#content' element.
10
+ # url and poll_interval are the initial params for this comment box
11
+
12
+ React.render(
13
+ React.create_element(
14
+ CommentBox, url: "comments.json", poll_interval: 2),
15
+ Element['#content']
16
+ )
17
+ end
18
+
19
+ class CommentBox
20
+
21
+ # A react component is simply a class that has a "render" method.
22
+
23
+ # But including React::Component mixin provides a nice dsl, and many other features
24
+
25
+ include React::Component
26
+
27
+ # Components can have parameters that are passed in when the component is first "mounted"
28
+ # and then updated as the application state changes. In this case url, and poll_interval will
29
+ # never change since this is the top level component.
30
+
31
+ required_param :url
32
+ required_param :poll_interval
33
+
34
+ # Components also may have internal state variables, which are like instance variables,
35
+ # with one added feature: Changing state causes a rerender to occur.
36
+
37
+ # The "comments" state is being initialized by parsing the javascript object at window.initial_comments
38
+ # This is not a react feature, but was just set up in the HTML header (see config.ru for how this was done).
39
+
40
+ define_state comments: JSON.from_object(`window.initial_comments`)
41
+
42
+ # The following call backs are made during the component lifecycle:
43
+
44
+ # before_mount before component is first rendered
45
+ # after_mount after component is first rendered, after DOM is loaded. ONLY CALLED ON CLIENT
46
+ # before_receive_props when component is being about to be rerendered by an outside state change. CANCELLABLE
47
+ # before_update just before a rerender, and not cancellable.
48
+ # after_update after DOM has been updated.
49
+ # before_unmount before component instance will be removed. Use this to kill low level handlers etc.
50
+
51
+ # just to show off how these callbacks work we have separated setting up a repeating fetch into three pieces.
52
+
53
+ # before mounting we will initialize a polling loop, but we don't want to start it yet.
54
+
55
+ before_mount do
56
+ @fetcher = every(poll_interval) do # we use the opal browser utility to call the server every poll_interval seconds
57
+ HTTP.get(url) do |response| # notice that params poll_interval, and url are accessed as instance methods
58
+ if response.ok?
59
+ comments! JSON.parse(response.body) # comments!(value) updates the state and notifies react of the state change
60
+ else
61
+ puts "failed with status #{response.status_code}"
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ # once we have things up and displayed lets start polling for updates
68
+
69
+ after_mount do
70
+ puts "start me up!"
71
+ @fetcher.start
72
+ end
73
+
74
+ # finally our component should be a good citizen and stop the polling when its unmounted
75
+
76
+ before_unmount do
77
+ @fetcher.stop
78
+ end
79
+
80
+ # components can have their own methods like any other class
81
+ # in this case we receive a new comment and send it the server
82
+
83
+ def send_comment_to_server(comment)
84
+ HTTP.post(url, payload: comment) do |response|
85
+ puts "failed with status #{response.status_code}" unless response.ok?
86
+ end
87
+ comment
88
+ end
89
+
90
+ # every component must implement a render method. The method must generate a single
91
+ # react virtual DOM element. React compares the output of each render and determines
92
+ # the minimum actual DOM update needed.
93
+
94
+ # A very common mistake is to try generate two or more elements (or none at all.) Either case will
95
+ # throw an error. Just remember that there is already a DOM node waiting for the output of the render
96
+ # hence the need for exactly one element per render.
97
+
98
+ def render
99
+
100
+ # the dsl syntax is simply a method call, with params hash, followed by a block
101
+ # the built in dsl methods correspond to the standard HTML5 tags such as div, h1, table, tr, td, span etc.
102
+ #return div.comment { h1 {"hello"} }
103
+ div class: "commentBox" do # just like <div class="commentBox">
104
+
105
+ h1 { "Comments" } # yep just like <h1>Comments</h1>
106
+
107
+ # Custom components use their class name, as the tag. Notice that the comments state is passed to
108
+ # to the CommentList component. This is the normal React paradigm: Data flows towards the leaf nodes.
109
+
110
+ CommentList comments: comments
111
+
112
+ # Sometimes its necessary for data to move upwards, and react provides several ways to do this.
113
+
114
+ # In this case we need to know when a new comment is submitted. So we pass a callback proc.
115
+
116
+ # The callback takes the new comment and sends it to the server and then pushes it onto the comments list.
117
+ # Again the comments! method is used to signal that the state is changing. The use of the "bang" pseudo
118
+ # operator is important as the value of comments has NOT changed (its still tha same array), but its
119
+ # internal state has.
120
+
121
+ CommentForm submit_comment: lambda { |comment| comments! << send_comment_to_server(comment)}
122
+
123
+ end
124
+ end
125
+
126
+ end
127
+
128
+ # Our second component!
129
+
130
+ class CommentList
131
+
132
+ include React::Component
133
+
134
+ # As we saw above a CommentList component takes a comments parameter
135
+ # Here we introduce optional parameter type checking. The syntax [Hash] means "Array of Hashes"
136
+ # In our case each comment is a hash with an author and text key.
137
+
138
+ # Failure to match the type puts a warning on the console not an error,
139
+ # and only in development mode not production.
140
+
141
+ required_param :comments, type: Array
142
+
143
+ # This is a good place to think more about the component lifecycle. The first time
144
+ # CommentList is mounted, comments will be the initial array of author, text hashes.
145
+ # As new comments are added the component will receive new params. However the component
146
+ # does NOT reinitialize its state. If changes in state are needed as result of incoming param changes
147
+ # the before_receive_props call back can be used.
148
+
149
+ def render
150
+
151
+ # Lets render some comments - all we need to do is iterate over the comments array using the usual
152
+ # ruby "each" method.
153
+
154
+ # This is a good place to clarify how the DSL works. Notice that we use comments.each NOT comments.collect
155
+ # When a tag method (such as div, or Comment) is called its "output" is internally pushed into a render buffer.
156
+ # This simplifies the DSL by separating the control flow from the output, but can sometimes be a bit confusing.
157
+
158
+ div.commentList.and_another_class.and_another do # you can also include the class haml style (tx to @dancinglightning!)
159
+ comments.each do |comment|
160
+ # By now we are getting used to the react paradigm: Stuff comes in, is processed, and then
161
+ # passed to next lower level. In this case we pass along each author-text pair to the Comment component.
162
+ Comment author: comment[:author], text: comment[:text], hash: comment
163
+ end
164
+ end
165
+ end
166
+
167
+ end
168
+
169
+ # Notice that the above CommentList component had no state. Each time its parameters change, it simply re-renders.
170
+ # CommentForm does have internal state as we will see...
171
+
172
+ class CommentForm
173
+
174
+ include React::Component
175
+
176
+ # While declaring the type of a param is optional its handy not only for debug, but also to let React create
177
+ # appropriate helpers based on the type. In this case we are passing in a Proc, and so React will treat the
178
+ # "submit_comment" param specially. Instead of submit_comment returning its value (as the previous params have done)
179
+ # it will call the associated Proc, thus allow CommentForm to communicate state changes back to the parent.
180
+
181
+ required_param :submit_comment, type: Proc
182
+
183
+ # We are going to have 2 state variable. One for each field in the comment. As the user types,
184
+ # these state variables will be updating causing a rerender of the CommentForm (but no other components.)
185
+
186
+ define_state :author, :text
187
+
188
+ def render
189
+ div do
190
+ div do
191
+
192
+ "Author: ".span # Note the shorthand for span { "Author" }. You can do this with br, span, th, td, and para (for p) tags
193
+
194
+ # Now we are going to generate an input tag. Notice how the author state variable is provided. Referencing
195
+ # author is what will cause us to re-render and update the input as the value of author changes.
196
+ # React will optimize the updates so parts that are not changing will not be effected.
197
+
198
+ input.author_name(type: :text, value: author, placeholder: "Your name", style: {width: "30%"}).
199
+ # and we attach an on_change handler to the input. As the input changes we simply update author.
200
+ on(:change) { |e| author! e.target.value }
201
+
202
+ end
203
+
204
+ div do
205
+ # lets have some fun with the text. Same deal as the author except we will use a text area...
206
+ div(style: {float: :left, width: "50%"}) do
207
+ textarea(value: text, placeholder: "Say something...", style: {width: "90%"}, rows: 30).
208
+ on(:change) { |e| text! e.target.value }
209
+ end
210
+ # and lets use Showdown to allow for markdown, and display the mark down to the left of input
211
+ # we will define Showdown later, and it will be our first reusable component, as we will use it twice.
212
+ div(style: {float: :left, width: "50%"}) do
213
+ Showdown markup: text
214
+ end
215
+ end
216
+
217
+ # Finally lets give the use a button to submit changes. Why not? We have come this far!
218
+ # Notice how the submit_comment proc param allows us to be ignorant of how the update is made.
219
+
220
+ # Notice that (author! "") updates author, but returns the current value.
221
+ # This is usually the desired behavior in React as we are typically interested in state changes,
222
+ # and before/after values, not simply doing a chained update of multiple variables.
223
+
224
+ button { "Post" }.on(:click) { submit_comment :author => (author! ""), :text => (text! "") }
225
+
226
+ end
227
+ end
228
+ end
229
+
230
+ # Wow only two more components left! This one is a breeze. We just take the author, and text and display
231
+ # them. We already know how to use our Showdown component to display the markdown so we can just reuse that.
232
+
233
+ class Comment
234
+
235
+ include React::Component
236
+
237
+ required_param :author
238
+ required_param :text
239
+ required_param :hash, type: Hash
240
+
241
+ def render
242
+ div.comment do
243
+ h2.comment_author { author } # NOTE: single underscores in haml style class names are converted to dashes
244
+ # so comment_author becomes comment-author, but comment__author would be comment_author
245
+ # this is handy for boot strap names like col-md-push-9 which can be written as col_md_push_9
246
+ Showdown markup: text
247
+ end
248
+ end
249
+
250
+ end
251
+
252
+ # Last but not least here is our ShowDown Component
253
+
254
+ class Showdown
255
+
256
+ include React::Component
257
+
258
+ required_param :markup
259
+
260
+ def render
261
+
262
+ # we will use some Opal lowlevel stuff to interface to the javascript Showdown class
263
+ # we only need to build the converter once, and then reuse it so we will use a plain old
264
+ # instance variable to keep track of it.
265
+
266
+ @converter ||= Native(`new Showdown.converter()`)
267
+
268
+ # then we will take our markup param, and convert it to html
269
+
270
+ raw_markup = @converter.makeHtml(markup) if markup
271
+
272
+ # React.js takes a very dim view of passing raw html so its purposefully made
273
+ # difficult so you won't do it by accident. After all think of how dangerous what we
274
+ # are doing right here is!
275
+
276
+ # The span tag can be replaced by any tag that could sensibly take a child html element.
277
+ # You could also use div, td, etc.
278
+
279
+ span(dangerously_set_inner_HTML: {__html: raw_markup})
280
+
281
+ end
282
+
283
+ end
284
+
285
+
286
+
287
+
288
+
289
+
290
+