volt 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rspec +2 -0
  4. data/.ruby-version +1 -0
  5. data/Gemfile +37 -0
  6. data/Guardfile +9 -0
  7. data/LICENSE.txt +22 -0
  8. data/Rakefile +23 -0
  9. data/Readme.md +34 -0
  10. data/VERSION +1 -0
  11. data/bin/volt +4 -0
  12. data/docs/GETTING_STARTED.md +7 -0
  13. data/docs/GUIDE.md +33 -0
  14. data/lib/volt.rb +15 -0
  15. data/lib/volt/benchmark/benchmark.rb +25 -0
  16. data/lib/volt/cli.rb +34 -0
  17. data/lib/volt/console.rb +19 -0
  18. data/lib/volt/controllers/model_controller.rb +29 -0
  19. data/lib/volt/extra_core/array.rb +10 -0
  20. data/lib/volt/extra_core/blank.rb +88 -0
  21. data/lib/volt/extra_core/extra_core.rb +7 -0
  22. data/lib/volt/extra_core/numeric.rb +9 -0
  23. data/lib/volt/extra_core/object.rb +36 -0
  24. data/lib/volt/extra_core/string.rb +29 -0
  25. data/lib/volt/extra_core/stringify_keys.rb +7 -0
  26. data/lib/volt/extra_core/true_false.rb +44 -0
  27. data/lib/volt/extra_core/try.rb +31 -0
  28. data/lib/volt/models.rb +5 -0
  29. data/lib/volt/models/array_model.rb +37 -0
  30. data/lib/volt/models/model.rb +210 -0
  31. data/lib/volt/models/model_wrapper.rb +23 -0
  32. data/lib/volt/models/params.rb +67 -0
  33. data/lib/volt/models/url.rb +192 -0
  34. data/lib/volt/page/url_tracker.rb +36 -0
  35. data/lib/volt/reactive/array_extensions.rb +13 -0
  36. data/lib/volt/reactive/event_chain.rb +126 -0
  37. data/lib/volt/reactive/events.rb +283 -0
  38. data/lib/volt/reactive/object_tracker.rb +99 -0
  39. data/lib/volt/reactive/object_tracking.rb +15 -0
  40. data/lib/volt/reactive/reactive_array.rb +222 -0
  41. data/lib/volt/reactive/reactive_tags.rb +64 -0
  42. data/lib/volt/reactive/reactive_value.rb +368 -0
  43. data/lib/volt/reactive/string_extensions.rb +34 -0
  44. data/lib/volt/router/routes.rb +83 -0
  45. data/lib/volt/server.rb +121 -0
  46. data/lib/volt/server/binding_setup.rb +2 -0
  47. data/lib/volt/server/channel_handler.rb +31 -0
  48. data/lib/volt/server/component_handler.rb +88 -0
  49. data/lib/volt/server/if_binding_setup.rb +29 -0
  50. data/lib/volt/server/request_handler.rb +16 -0
  51. data/lib/volt/server/scope.rb +43 -0
  52. data/lib/volt/server/source_map_server.rb +31 -0
  53. data/lib/volt/server/template_parser.rb +452 -0
  54. data/lib/volt/store/mongo.rb +5 -0
  55. data/lib/volt/templates/attribute_binding.rb +110 -0
  56. data/lib/volt/templates/base_binding.rb +37 -0
  57. data/lib/volt/templates/channel.rb +48 -0
  58. data/lib/volt/templates/content_binding.rb +35 -0
  59. data/lib/volt/templates/document_events.rb +80 -0
  60. data/lib/volt/templates/each_binding.rb +115 -0
  61. data/lib/volt/templates/event_binding.rb +51 -0
  62. data/lib/volt/templates/if_binding.rb +74 -0
  63. data/lib/volt/templates/memory_test.rb +26 -0
  64. data/lib/volt/templates/page.rb +146 -0
  65. data/lib/volt/templates/reactive_template.rb +38 -0
  66. data/lib/volt/templates/render_queue.rb +5 -0
  67. data/lib/volt/templates/sub_context.rb +23 -0
  68. data/lib/volt/templates/targets/attribute_section.rb +33 -0
  69. data/lib/volt/templates/targets/attribute_target.rb +18 -0
  70. data/lib/volt/templates/targets/base_section.rb +14 -0
  71. data/lib/volt/templates/targets/binding_document/base_node.rb +3 -0
  72. data/lib/volt/templates/targets/binding_document/component_node.rb +112 -0
  73. data/lib/volt/templates/targets/binding_document/html_node.rb +11 -0
  74. data/lib/volt/templates/targets/dom_section.rb +147 -0
  75. data/lib/volt/templates/targets/dom_target.rb +11 -0
  76. data/lib/volt/templates/template_binding.rb +159 -0
  77. data/lib/volt/templates/template_renderer.rb +50 -0
  78. data/spec/models/event_chain_spec.rb +129 -0
  79. data/spec/models/model_spec.rb +340 -0
  80. data/spec/models/old_model_spec.rb +109 -0
  81. data/spec/models/reactive_array_spec.rb +262 -0
  82. data/spec/models/reactive_tags_spec.rb +35 -0
  83. data/spec/models/reactive_value_spec.rb +336 -0
  84. data/spec/models/string_extensions_spec.rb +57 -0
  85. data/spec/router/routes_spec.rb +24 -0
  86. data/spec/server/template_parser_spec.rb +50 -0
  87. data/spec/spec_helper.rb +20 -0
  88. data/spec/store/mongo_spec.rb +4 -0
  89. data/spec/templates/targets/binding_document/component_node_spec.rb +18 -0
  90. data/spec/templates/template_binding_spec.rb +98 -0
  91. data/templates/.gitignore +12 -0
  92. data/templates/Gemfile.tt +8 -0
  93. data/templates/app/.empty_directory +0 -0
  94. data/templates/app/home/config/routes.rb +1 -0
  95. data/templates/app/home/controllers/index_controller.rb +5 -0
  96. data/templates/app/home/css/.empty_directory +0 -0
  97. data/templates/app/home/models/.empty_directory +0 -0
  98. data/templates/app/home/views/index/about.html +9 -0
  99. data/templates/app/home/views/index/home.html +7 -0
  100. data/templates/app/home/views/index/index.html +28 -0
  101. data/templates/config.ru +4 -0
  102. data/templates/public/css/ansi.css +0 -0
  103. data/templates/public/css/bootstrap-theme.css +459 -0
  104. data/templates/public/css/bootstrap.css +7098 -0
  105. data/templates/public/css/jumbotron.css +79 -0
  106. data/templates/public/fonts/glyphicons-halflings-regular.eot +0 -0
  107. data/templates/public/fonts/glyphicons-halflings-regular.svg +229 -0
  108. data/templates/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  109. data/templates/public/fonts/glyphicons-halflings-regular.woff +0 -0
  110. data/templates/public/index.html +25 -0
  111. data/templates/public/js/bootstrap.js +0 -0
  112. data/templates/public/js/jquery-2.0.3.js +8829 -0
  113. data/templates/public/js/sockjs-0.2.1.min.js +27 -0
  114. data/templates/spec/spec_helper.rb +20 -0
  115. data/volt.gemspec +41 -0
  116. metadata +412 -0
@@ -0,0 +1,31 @@
1
+ class SourceMapServer
2
+ def initialize sprockets
3
+ @sprockets = sprockets
4
+ end
5
+
6
+ attr_reader :sprockets
7
+
8
+ attr_writer :prefix
9
+
10
+ def prefix
11
+ @prefix ||= '/__opal_source_maps__'
12
+ end
13
+
14
+ def inspect
15
+ "#<#{self.class}:#{object_id}>"
16
+ end
17
+
18
+ def call(env)
19
+ path_info = env['PATH_INFO']
20
+
21
+ if path_info =~ /\.js\.map$/
22
+ path = env['PATH_INFO'].gsub(/^\/|\.js\.map$/, '')
23
+ asset = sprockets[path]
24
+ return [404, {}, []] if asset.nil?
25
+
26
+ return [200, {"Content-Type" => "text/json"}, [$OPAL_SOURCE_MAPS[asset.pathname].to_s]]
27
+ else
28
+ return [200, {"Content-Type" => "text/text"}, [File.read(sprockets.resolve(path_info))]]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,452 @@
1
+ require 'volt/server/scope'
2
+ require 'volt/server/if_binding_setup'
3
+ require 'nokogiri'
4
+
5
+ # TODO: The section_name that we're passing in should probably be
6
+ # abstracted out. Possibly this whole thing needs a rewrite.
7
+
8
+ class Template
9
+ attr_accessor :current_scope, :section_name
10
+
11
+ def initialize(template_parser, section_name, template, scope=Scope.new)
12
+ @binding_number = 0
13
+
14
+ @template_parser = template_parser
15
+ @section_name = section_name
16
+ @template = template
17
+ @scopes = [scope]
18
+ @current_scope = @scopes.first
19
+ end
20
+
21
+ def html
22
+ if @template.respond_to?(:name) && @template.name[0] == ':'
23
+ # Don't return the <:section> tags
24
+ return @template.children.to_html
25
+ else
26
+ # if @template.class == Nokogiri::XML::NodeSet
27
+ # result = ''
28
+ # @template.each do |node|
29
+ # result << node.to_html
30
+ # end
31
+ # else
32
+ result = @template.to_html
33
+ # end
34
+ result
35
+ end
36
+ end
37
+
38
+
39
+ def add_binding(node, content)
40
+ if content[0] == '/'
41
+ add_close_mustache(node)
42
+ elsif content[0] == '#'
43
+ command, *content = content.split(/ /)
44
+ content = content.join(' ')
45
+
46
+ case command
47
+ when '#template'
48
+ return add_template(node, content)
49
+ when '#each'
50
+ return add_each_binding(node, content)
51
+ when '#if'
52
+ return add_if_binding(node, content)
53
+ when '#elsif'
54
+ return add_else_binding(node, content)
55
+ when '#else'
56
+ if content.present?
57
+ # TODO: improve error, include line/file
58
+ raise "#else should not include a condition, use #elsif instead. #{content} was passed as a condition."
59
+ end
60
+
61
+ return add_else_binding(node, nil)
62
+ else
63
+ # TODO: Handle invalid command
64
+ raise "Invalid Command"
65
+ end
66
+ else
67
+ # text binding
68
+ return add_text_binding(content)
69
+ end
70
+ end
71
+
72
+ def add_template(node, content)
73
+ html = "<!-- $#{@binding_number} --><!-- $/#{@binding_number} -->"
74
+
75
+ @current_scope.add_binding(@binding_number, "lambda { |target, context, id| TemplateBinding.new(target, context, id, #{@template_parser.template_path.inspect}, Proc.new { [#{content}] }) }")
76
+
77
+ @binding_number += 1
78
+ return html
79
+ end
80
+
81
+ def add_each_binding(node, content)
82
+ html = "<!-- $#{@binding_number} -->"
83
+
84
+ content, variable_name = content.strip.split(/ as /)
85
+
86
+ template_name = "#{@template_parser.template_path}/#{section_name}/__template/#{@binding_number}"
87
+ @current_scope.add_binding(@binding_number, "lambda { |target, context, id| EachBinding.new(target, context, id, Proc.new { #{content} }, #{variable_name.inspect}, #{template_name.inspect}) }")
88
+
89
+ # Add the node, the binding number, then store the location where the
90
+ # bindings for this block starts.
91
+ @current_scope = Scope.new(@binding_number)
92
+ @scopes << @current_scope
93
+
94
+ @binding_number += 1
95
+ return html
96
+ end
97
+
98
+ def add_if_binding(node, content)
99
+ html = "<!-- $#{@binding_number} -->"
100
+
101
+ template_name = "#{@template_parser.template_path}/#{section_name}/__template/#{@binding_number}"
102
+ if_binding_setup = IfBindingSetup.new
103
+ if_binding_setup.add_branch(content, template_name)
104
+
105
+ @current_scope.start_if_binding(@binding_number, if_binding_setup)
106
+
107
+ # Add the node, the binding number, then store the location where the
108
+ # bindings for this block starts.
109
+ @current_scope = Scope.new(@binding_number)
110
+ @scopes << @current_scope
111
+
112
+ @binding_number += 1
113
+ return html
114
+ end
115
+
116
+ def add_else_binding(node, content)
117
+ html = add_close_mustache(node, false)
118
+
119
+ html += "<!-- $#{@binding_number} -->"
120
+ template_name = "#{@template_parser.template_path}/#{section_name}/__template/#{@binding_number}"
121
+
122
+ @current_scope.current_if_binding[1].add_branch(content, template_name)
123
+
124
+ # Add the node, the binding number, then store the location where the
125
+ # bindings for this block starts.
126
+ @current_scope = Scope.new(@binding_number)
127
+ @scopes << @current_scope
128
+
129
+ @binding_number += 1
130
+
131
+ return html
132
+ end
133
+
134
+ def add_close_mustache(node, close_if=true)
135
+ scope = @scopes.pop
136
+ @current_scope = @scopes.last
137
+
138
+ # Close an outstanding if binding (if it exists)
139
+ @current_scope.close_if_binding! if close_if
140
+
141
+ # Track that this scope was closed out
142
+ @current_scope.add_closed_child_scope(scope)
143
+
144
+ html = "<!-- $/#{scope.outer_binding_number} -->"
145
+
146
+ return html
147
+ end
148
+
149
+ # When we find a binding, we pass it's content in here and replace it with
150
+ # the return value
151
+ def add_text_binding(content)
152
+ html = "<!-- $#{@binding_number} --><!-- $/#{@binding_number} -->"
153
+
154
+ @current_scope.add_binding(@binding_number, "lambda { |target, context, id| ContentBinding.new(target, context, id, Proc.new { #{content} }) }")
155
+
156
+ @binding_number += 1
157
+ return html
158
+ end
159
+
160
+ def setup_node_id(node)
161
+ id = node['id']
162
+ # First assign this node an id if it doesn't have one
163
+ unless id
164
+ id = node['id'] = "id#{@binding_number}"
165
+ @binding_number += 1
166
+ end
167
+ end
168
+
169
+ # Attribute bindings support multiple handlebar listeners
170
+ # Exvoltle:
171
+ # <button click="{_primary} {_important}">...
172
+ #
173
+ # To accomplish this, we create a new listener from the existing ones in the Proc
174
+ # that we pass to the binding when it is created.
175
+ def add_attribute_binding(node, attribute, content)
176
+ setup_node_id(node)
177
+
178
+ if content =~ /^\{[^\{]+\}$/
179
+ # Getter is the content inside of { ... }
180
+ add_single_getter(node, attribute, content)
181
+ else
182
+ add_multiple_getters(node, attribute, content)
183
+ end
184
+
185
+ end
186
+
187
+ def add_single_getter(node, attribute, content)
188
+ if attribute == 'checked' || true
189
+ # For a checkbox, we don't want to add
190
+ getter = content[1..-2]
191
+ else
192
+ # Otherwise we should combine them
193
+ # TODO: We should make .or handle assignment
194
+ getter = "_tmp = #{content[1..-2]}.or('') ; _tmp.reactive_manager.setter! { |val| self.#{content[1..-2]} = val } ; _tmp"
195
+ end
196
+
197
+ @current_scope.add_binding(node['id'], "lambda { |target, context, id| AttributeBinding.new(target, context, id, #{attribute.inspect}, Proc.new { #{getter} }) }")
198
+ end
199
+
200
+ def add_multiple_getters(node, attribute, content)
201
+ case attribute
202
+ when 'checked', 'value'
203
+ if parts.size > 1
204
+ # Multiple ReactiveValue's can not be passed to value or checked attributes.
205
+ raise "Multiple bindings can not be passed to a #{attribute} binding."
206
+ end
207
+ end
208
+
209
+ reactive_template_path = add_reactive_template(content)
210
+
211
+ @current_scope.add_binding(node['id'], "lambda { |target, context, id| AttributeBinding.new(target, context, id, #{attribute.inspect}, Proc.new { ReactiveTemplate.new(context, #{reactive_template_path.inspect}) }) }")
212
+ end
213
+
214
+ # Returns a path to a template for the content. This can be passed
215
+ # into ReactiveTemplate.new, along with the current context.
216
+ def add_reactive_template(content)
217
+ # Return a template path instead
218
+ template_name = "__attribute/#{@binding_number}"
219
+ full_template_path = "#{@template_parser.template_path}/#{section_name}/#{template_name}"
220
+ @binding_number += 1
221
+
222
+ attribute_template = Template.new(@template_parser, section_name, Nokogiri::HTML::DocumentFragment.parse(content))
223
+ @template_parser.add_template("#{section_name}/#{template_name}", attribute_template)
224
+ attribute_template.start_walk
225
+ attribute_template.pull_closed_block_scopes
226
+
227
+ return full_template_path
228
+ end
229
+
230
+ def add_event_binding(node, attribute_name, content)
231
+ setup_node_id(node)
232
+
233
+ event = attribute_name[2..-1]
234
+
235
+ if node.name == 'a'
236
+ # For links, we need to add blank href to make it clickable.
237
+ node['href'] ||= ''
238
+ end
239
+
240
+ @current_scope.add_binding(node['id'], "lambda { |target, context, id| EventBinding.new(target, context, id, #{event.inspect}, Proc.new {|event| #{content} })}")
241
+ end
242
+
243
+ def pull_closed_block_scopes(scope=@current_scope)
244
+ if scope.closed_block_scopes
245
+ scope.closed_block_scopes.each do |sub_scope|
246
+ # Loop through any subscopes first, pull them in.
247
+ pull_closed_block_scopes(sub_scope)
248
+
249
+ # Grab everything between the start/end html comments
250
+ start_node = find_by_comment("$#{sub_scope.outer_binding_number}")
251
+ end_node = find_by_comment("$/#{sub_scope.outer_binding_number}")
252
+
253
+ move_nodes_to_new_template(start_node, end_node, sub_scope)
254
+ end
255
+ end
256
+ end
257
+
258
+
259
+ def move_nodes_to_new_template(start_node, end_node, scope)
260
+ # TODO: currently this doesn't handle spanning nodes within seperate containers.
261
+ # so doing tr's doesn't work for some reason.
262
+
263
+ start_parent = start_node.parent
264
+ start_parent = start_parent.children if start_parent.is_a?(Nokogiri::HTML::DocumentFragment) || start_parent.is_a?(Nokogiri::XML::Element)
265
+ start_index = start_parent.index(start_node) + 1
266
+
267
+ end_parent = end_node.parent
268
+ end_parent = end_parent.children if end_parent.is_a?(Nokogiri::HTML::DocumentFragment) || end_parent.is_a?(Nokogiri::XML::Element)
269
+ end_index = end_parent.index(end_node) - 1
270
+
271
+ move_nodes = start_parent[start_index..end_index]
272
+ move_nodes.remove
273
+
274
+ new_template = Template.new(@template_parser, section_name, move_nodes, scope)
275
+
276
+ @template_parser.add_template("#{section_name}/__template/#{scope.outer_binding_number}", new_template)
277
+ end
278
+
279
+
280
+ def find_by_comment(name)
281
+ return @template.xpath("descendant::comment()[. = ' #{name} ']").first
282
+ end
283
+
284
+ def start_walk
285
+ walk(@template)
286
+ end
287
+
288
+ # We implement a dom walker that can walk down the dom and spit out output
289
+ # html as we go
290
+ def walk(node)
291
+ case node.type
292
+ when 1
293
+ # html node
294
+ walk_html_node(node)
295
+ when 3
296
+ # text node
297
+ walk_text_node(node)
298
+ end
299
+
300
+ node.children.each do |child|
301
+ walk(child)
302
+ end
303
+ end
304
+
305
+ def walk_html_node(node)
306
+ if node.name[0] == ':' && node.path.count('/') > 1
307
+ parse_component(node)
308
+ elsif node.name == 'textarea'
309
+ parse_textarea(node)
310
+ else
311
+ parse_html_node(node)
312
+ end
313
+ end
314
+
315
+ # We provide a quick way to render components with tags starting
316
+ # with a :
317
+ # Count the number of /'s in the path, if we are at the root node
318
+ # we can ignore it, since this is the template its self.
319
+ # TODO: Root node might not be the template if we parsed directly
320
+ # without a subtemplate specifier. We need to find a good way to
321
+ # parse only within the subtemplate.
322
+ def parse_component(node)
323
+ template_path = node.name[1..-1].gsub(':', '/')
324
+
325
+ # Take the attributes and turn them into a hash
326
+ attribute_hash = {}
327
+ node.attribute_nodes.each do |attribute_node|
328
+ content = attribute_node.value
329
+
330
+ if !content.index('{')
331
+ # passing in a string
332
+ value = content.inspect
333
+ elsif content =~ /^\{[^\}]+\}$/
334
+ # Has one binding, just get it
335
+ value = "Proc.new { #{content[1..-2]} }"
336
+ else
337
+ # Has multiple bindings, we need to render a template here
338
+ attr_template_path = add_reactive_template(content)
339
+
340
+ value = "Proc.new { ReactiveTemplate.new(context, #{attr_template_path.inspect}) }"
341
+ end
342
+
343
+ attribute_hash[attribute_node.name] = value
344
+ end
345
+
346
+ attributes_string = attribute_hash.to_a.map do |key, value|
347
+ "#{key.inspect} => #{value}"
348
+ end.join(', ')
349
+
350
+ # Setup the arguments string, which goes to the TemplateBinding
351
+ args_str = "#{template_path.inspect}"
352
+ args_str << ", {#{attributes_string}}" if attribute_hash.size > 0
353
+
354
+ new_html = add_template(node, args_str)
355
+
356
+ node.swap(new_html)#Nokogiri::HTML::DocumentFragment.parse(new_html))
357
+ end
358
+
359
+ def parse_textarea(node)
360
+ # The contents of textareas should really be treated like a
361
+ # value= attribute. So here we pull the content into a value attribute
362
+ # if the textarea has bindings in the content.
363
+ if node.inner_html =~ /\{[^\}]+\}/
364
+ node[:value] = node.inner_html
365
+ node.children.remove
366
+ end
367
+
368
+ parse_html_node(node)
369
+ end
370
+
371
+ def parse_html_node(node)
372
+ node.attribute_nodes.each do |attribute_node|
373
+ if attribute_node.name =~ /^e\-/
374
+ # We have an e- binding
375
+ add_event_binding(node, attribute_node.name, attribute_node.value)
376
+
377
+ # remove the attribute
378
+ attribute_node.remove
379
+ elsif attribute_node.value.match(/\{[^\}]+\}/)
380
+ # Has bindings
381
+ add_attribute_binding(node, attribute_node.name, attribute_node.value)
382
+
383
+ # remove the attribute
384
+ attribute_node.remove
385
+ end
386
+ end
387
+ end
388
+
389
+ def walk_text_node(node)
390
+ new_html = node.content.gsub(/\{([^\}]+)\}/) do |template_binding|
391
+ add_binding(node, $1)
392
+ end
393
+
394
+ # puts "------! #{new_html.inspect} - #{node.class.inspect} - #{node.inspect}"
395
+
396
+ # TODO: Broke here in jruby
397
+ node.swap(new_html)# if new_html.blank?
398
+
399
+ #Nokogiri::HTML::DocumentFragment.parse(new_html))
400
+ end
401
+ end
402
+
403
+ class TemplateParser
404
+ attr_accessor :dom, :bindings, :template_path
405
+
406
+ def initialize(template, template_path)
407
+ @templates = {}
408
+ @template_path = template_path
409
+
410
+ template_fragment = Nokogiri::HTML::DocumentFragment.parse(template)
411
+
412
+ # Add templates for each section
413
+
414
+ # Check for sections
415
+ sections = []
416
+ if template_fragment.children[0].name[0] == ':'
417
+ template_fragment.children.each do |child|
418
+ if child.is_a?(Nokogiri::XML::Element)
419
+ sections << [child, child.name[1..-1]]
420
+ end
421
+ end
422
+ else
423
+ sections << [template_fragment, 'body']
424
+ end
425
+
426
+ sections.each do |section, name|
427
+ template = Template.new(self, name, section)
428
+ add_template(name, template)
429
+ template.start_walk
430
+ template.pull_closed_block_scopes
431
+ end
432
+
433
+ end
434
+
435
+ def add_template(name, template)
436
+ @templates[@template_path + '/' + name] = template
437
+ end
438
+
439
+ # Return the templates, but map the html from nokogiri to html
440
+ def templates
441
+ mapped = {}
442
+ @templates.each_pair do |name, template|
443
+ mapped[name] = {
444
+ 'html' => template.html,
445
+ 'bindings' => template.current_scope.bindings
446
+ }
447
+ end
448
+
449
+ return mapped
450
+ end
451
+
452
+ end