apotomo 0.1.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 (74) hide show
  1. data/Gemfile +10 -0
  2. data/Gemfile.lock +47 -0
  3. data/README +141 -0
  4. data/README.rdoc +141 -0
  5. data/Rakefile +78 -0
  6. data/TODO +36 -0
  7. data/app/cells/apotomo/child_switch_widget/switch.html.erb +1 -0
  8. data/app/cells/apotomo/child_switch_widget/switch.rhtml +1 -0
  9. data/app/cells/apotomo/deep_link_widget.rb +27 -0
  10. data/app/cells/apotomo/deep_link_widget/setup.html.erb +20 -0
  11. data/app/cells/apotomo/java_script_widget.rb +12 -0
  12. data/app/cells/apotomo/tab_panel_widget.rb +87 -0
  13. data/app/cells/apotomo/tab_panel_widget/display.html.erb +57 -0
  14. data/app/cells/apotomo/tab_widget.rb +18 -0
  15. data/app/cells/apotomo/tab_widget/display.html.erb +1 -0
  16. data/config/routes.rb +3 -0
  17. data/generators/widget/USAGE +15 -0
  18. data/generators/widget/templates/functional_test.rb +8 -0
  19. data/generators/widget/templates/view.html.erb +2 -0
  20. data/generators/widget/templates/view.html.haml +3 -0
  21. data/generators/widget/templates/widget.rb +8 -0
  22. data/generators/widget/widget_generator.rb +34 -0
  23. data/lib/apotomo.rb +59 -0
  24. data/lib/apotomo/caching.rb +37 -0
  25. data/lib/apotomo/container_widget.rb +10 -0
  26. data/lib/apotomo/deep_link_methods.rb +90 -0
  27. data/lib/apotomo/event.rb +9 -0
  28. data/lib/apotomo/event_handler.rb +23 -0
  29. data/lib/apotomo/event_methods.rb +102 -0
  30. data/lib/apotomo/invoke_event_handler.rb +24 -0
  31. data/lib/apotomo/javascript_generator.rb +57 -0
  32. data/lib/apotomo/persistence.rb +139 -0
  33. data/lib/apotomo/proc_event_handler.rb +18 -0
  34. data/lib/apotomo/rails/controller_methods.rb +161 -0
  35. data/lib/apotomo/rails/view_helper.rb +95 -0
  36. data/lib/apotomo/rails/view_methods.rb +7 -0
  37. data/lib/apotomo/request_processor.rb +92 -0
  38. data/lib/apotomo/stateful_widget.rb +8 -0
  39. data/lib/apotomo/transition.rb +46 -0
  40. data/lib/apotomo/tree_node.rb +186 -0
  41. data/lib/apotomo/version.rb +5 -0
  42. data/lib/apotomo/widget.rb +289 -0
  43. data/lib/apotomo/widget_shortcuts.rb +36 -0
  44. data/rails/init.rb +0 -0
  45. data/test/fixtures/application_widget_tree.rb +2 -0
  46. data/test/rails/controller_methods_test.rb +206 -0
  47. data/test/rails/rails_integration_test.rb +99 -0
  48. data/test/rails/view_helper_test.rb +77 -0
  49. data/test/rails/view_methods_test.rb +40 -0
  50. data/test/rails/widget_generator_test.rb +47 -0
  51. data/test/support/assertions_helper.rb +13 -0
  52. data/test/support/test_case_methods.rb +68 -0
  53. data/test/test_helper.rb +77 -0
  54. data/test/unit/apotomo_test.rb +20 -0
  55. data/test/unit/container_test.rb +20 -0
  56. data/test/unit/event_handler_test.rb +67 -0
  57. data/test/unit/event_methods_test.rb +83 -0
  58. data/test/unit/event_test.rb +30 -0
  59. data/test/unit/invoke_test.rb +123 -0
  60. data/test/unit/javascript_generator_test.rb +90 -0
  61. data/test/unit/onfire_integration_test.rb +19 -0
  62. data/test/unit/persistence_test.rb +240 -0
  63. data/test/unit/render_test.rb +203 -0
  64. data/test/unit/request_processor_test.rb +178 -0
  65. data/test/unit/stateful_widget_test.rb +135 -0
  66. data/test/unit/test_addressing.rb +111 -0
  67. data/test/unit/test_caching.rb +54 -0
  68. data/test/unit/test_jump_to_state.rb +89 -0
  69. data/test/unit/test_tab_panel.rb +72 -0
  70. data/test/unit/test_widget_shortcuts.rb +45 -0
  71. data/test/unit/transition_test.rb +33 -0
  72. data/test/unit/widget_shortcuts_test.rb +68 -0
  73. data/test/unit/widget_test.rb +24 -0
  74. metadata +215 -0
@@ -0,0 +1,9 @@
1
+ module Apotomo
2
+ # Events are created by Apotomo in #fire. They bubble up from their source to root and trigger
3
+ # event handlers.
4
+ class Event < Onfire::Event
5
+ def _dump(depth)
6
+ raise "You're trying to serialize an instance of Apotomo::Event. Don't do that."
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,23 @@
1
+ module Apotomo
2
+ # EventHandlers are "callbacks", not knowing why they exist, but what to do.
3
+ class EventHandler
4
+
5
+ def process_event(event)
6
+ # do something, and return content.
7
+ nil
8
+ end
9
+
10
+ def ==(other)
11
+ self.to_s == other.to_s
12
+ end
13
+
14
+ # Invoked by Onfire.
15
+ def call(event)
16
+ event.source.root.page_updates << process_event(event)
17
+ end
18
+
19
+ end
20
+ end
21
+
22
+ require 'apotomo/invoke_event_handler'
23
+ require 'apotomo/proc_event_handler'
@@ -0,0 +1,102 @@
1
+ require 'apotomo/event_handler'
2
+
3
+ module Apotomo
4
+ # Introduces event-processing functions into the StatefulWidget.
5
+
6
+ module EventMethods
7
+ attr_writer :page_updates
8
+ # Replacement for the EventProcessor singleton queue.
9
+ def page_updates
10
+ @page_updates ||= []
11
+ end
12
+
13
+ def self.included(base)
14
+ base.extend(ClassMethods)
15
+ base.initialize_hooks << :add_class_event_handlers
16
+ end
17
+
18
+ def add_class_event_handlers(*)
19
+ self.class.responds_to_event_options.each { |options| respond_to_event(*options) }
20
+ end
21
+
22
+ module ClassMethods
23
+ def responds_to_event(*options)
24
+ responds_to_event_options << options
25
+ end
26
+ alias_method :respond_to_event, :responds_to_event
27
+
28
+ def responds_to_event_options
29
+ @responds_to_event_options ||= []
30
+ end
31
+ end
32
+ # Instructs the widget to look out for <tt>type</tt> Events that are passing by while bubbling.
33
+ # If an appropriate event is encountered the widget will send the targeted widget (or itself) to another
34
+ # state, which implies an update of the invoked widget.
35
+ #
36
+ # You may configure the event handler with the following <tt>options</tt>:
37
+ # :with => (required) the state to invoke on the target widget
38
+ # :on => (optional) the targeted widget's id, defaults to <tt>self.name</tt>
39
+ # :from => (optional) the source id of the widget that triggered the event, defaults to any widget
40
+ #
41
+ # Example:
42
+ #
43
+ # trap = cell(:input_field, :smell_like_cheese, 'mouse_trap')
44
+ # trap.respond_to_event :mouseOver, :with => :catch_mouse
45
+ #
46
+ # This would instruct <tt>trap</tt> to catch a <tt>:mouseOver</tt> event from any widget (including itself) and
47
+ # to invoke the state <tt>:catch_mouse</tt> on itself as trigger.
48
+ #
49
+ #
50
+ # hunter = cell(:form, :hunt_for_mice, 'my_form')
51
+ # hunter << cell(:input_field, :smell_like_cheese, 'mouse_trap')
52
+ # hunter << cell(:text_area, :stick_like_honey, 'bear_trap')
53
+ # hunter.respond_to_event :captured, :from => 'mouse_trap', :with => :refill_cheese, :on => 'mouse_trap'
54
+ #
55
+ # As both the bear- and the mouse trap can trigger a <tt>:captured</tt> event the later <tt>respond_to_event</tt>
56
+ # would invoke <tt>:refill_cheese</tt> on the <tt>mouse_trap</tt> widget as soon as this and only this widget fired.
57
+ # It is important to understand the <tt>:from</tt> parameter as it filters the event source - it wouldn't make
58
+ # sense to refill the mouse trap if the bear trap snapped, would it?
59
+
60
+ def respond_to_event(type, options)
61
+ options[:once] = true if options[:once].nil?
62
+
63
+ handler_opts = {}
64
+ handler_opts[:widget_id] = options[:on] || self.name
65
+ handler_opts[:state] = options[:with]
66
+
67
+ handler = InvokeEventHandler.new(handler_opts)
68
+
69
+ return if options[:once] and event_table.all_handlers_for(type, options[:from]).include?(handler)
70
+
71
+ on(type, :do => handler, :from => options[:from])
72
+ end
73
+
74
+ def trigger(*args)
75
+ fire(*args)
76
+ end
77
+
78
+ # Invokes <tt>state</tt> on the widget <em>and</end> updates itself on the page. This should
79
+ # never be called from outside but in setters when some internal value changed and must be
80
+ # displayed instantly.
81
+ #
82
+ # Implements the following pattern (TODO: remove example as soon as invoke! proofed):
83
+ #
84
+ # def title=(str)
85
+ # @title = str
86
+ # peek(:update, self.name, :display, self.name)
87
+ # trigger(:update)
88
+ # end
89
+ def invoke!(state)
90
+ ### TODO: encapsulate in PageUpdateQueue:
91
+ Apotomo::EventProcessor.instance.processed_handlers << [name, invoke(:state)]
92
+ end
93
+
94
+
95
+
96
+ protected
97
+ # Get all handlers from self for the passed event (overriding Onfire#local_event_handlers).
98
+ def local_event_handlers(event)
99
+ event_table.all_handlers_for(event.type, event.source.name) # we key with widget_id.
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,24 @@
1
+ module Apotomo
2
+ class InvokeEventHandler < EventHandler
3
+ attr_accessor :widget_id, :state
4
+
5
+ def initialize(opts={})
6
+ @widget_id = opts.delete(:widget_id)
7
+ @state = opts.delete(:state)
8
+ end
9
+
10
+ def process_event(event)
11
+ target = event.source.root.find_by_path(widget_id) ### DISCUSS: widget_id or widget_selector?
12
+
13
+ #::Rails.logger.debug "EventHandler: invoking #{target.name}##{state}"
14
+ ### DISCUSS: let target access event?
15
+ ### pass additional opts to #invoke?
16
+ ### DISCUSS: pass block here?
17
+ target.opts[:event] = event
18
+
19
+ target.invoke(state)
20
+ end
21
+
22
+ def to_s; "InvokeEventHandler:#{widget_id}##{state}"; end
23
+ end
24
+ end
@@ -0,0 +1,57 @@
1
+ module Apotomo
2
+ class JavascriptGenerator
3
+ def initialize(framework)
4
+ raise "No JS framework specified" if framework.blank?
5
+ extend "apotomo/javascript_generator/#{framework}".camelize.constantize
6
+ end
7
+
8
+ def <<(javascript)
9
+ "#{javascript}"
10
+ end
11
+
12
+ # Copied from ActionView::Helpers::JavascriptHelper.
13
+ JS_ESCAPE_MAP = {
14
+ '\\' => '\\\\',
15
+ '</' => '<\/',
16
+ "\r\n" => '\n',
17
+ "\n" => '\n',
18
+ "\r" => '\n',
19
+ '"' => '\\"',
20
+ "'" => "\\'" }
21
+
22
+ # Escape carrier returns and single and double quotes for JavaScript segments.
23
+ def self.escape(javascript)
24
+ return javascript.gsub(/(\\|<\/|\r\n|[\n\r"'])/) { JS_ESCAPE_MAP[$1] } if javascript
25
+
26
+ ''
27
+ end
28
+
29
+ def escape(javascript)
30
+ self.class.escape(javascript)
31
+ end
32
+
33
+ module Prototype
34
+ def prototype; end
35
+ def element(id); "$(\"#{id}\")"; end
36
+ def xhr(url); "new Ajax.Request(\"#{url}\")"; end
37
+ def update(id, markup); element(id) + '.update("'+escape(markup)+'")'; end
38
+ def replace(id, markup); element(id) + '.replace("'+escape(markup)+'")'; end
39
+ end
40
+
41
+ module Right
42
+ def right; end
43
+ def element(id); "$(\"#{id}\")"; end
44
+ def xhr(url); "new Xhr(\"#{url}\", {evalScripts:true}).send()"; end
45
+ def update(id, markup); element(id) + '.update("'+escape(markup)+'")'; end
46
+ def replace(id, markup); element(id) + '.replace("'+escape(markup)+'")'; end
47
+ end
48
+
49
+ module Jquery
50
+ def jquery; end
51
+ def element(id); "$(\"##{id}\")"; end
52
+ def xhr(url); "$.ajax({url: \"#{url}\"})"; end
53
+ def update(id, markup); element(id) + '.html("'+escape(markup)+'")'; end
54
+ def replace(id, markup); element(id) + '.replaceWith("'+escape(markup)+'")'; end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,139 @@
1
+ module Apotomo
2
+ # Methods needed to serialize the widget tree and back.
3
+ module Persistence
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # For Ruby 1.8/1.9 compatibility.
9
+ def symbolized_instance_variables
10
+ instance_variables.map { |ivar| ivar.to_sym }
11
+ end
12
+
13
+ def freeze_ivars_to(storage)
14
+ frozen = {}
15
+ (symbolized_instance_variables - unfreezable_ivars).each do |ivar|
16
+ frozen[ivar] = instance_variable_get(ivar)
17
+ end
18
+ storage[path] = frozen
19
+ end
20
+
21
+ ### FIXME: rewrite so that root might be stateless as well.
22
+ def freeze_data_to(storage)
23
+ freeze_ivars_to(storage)# if self.kind_of?(StatefulWidget)
24
+ children.each { |child| child.freeze_data_to(storage) if child.kind_of?(StatefulWidget) }
25
+ end
26
+
27
+ def freeze_to(storage)
28
+ storage[:apotomo_root] = self # save structure.
29
+ storage[:apotomo_widget_ivars] = {}
30
+ freeze_data_to(storage[:apotomo_widget_ivars]) # save ivars.
31
+ end
32
+
33
+ def thaw_ivars_from(storage)
34
+ storage.fetch(path, {}).each do |k, v|
35
+ instance_variable_set(k, v)
36
+ end
37
+ end
38
+
39
+ def thaw_data_from(storage)
40
+ thaw_ivars_from(storage)
41
+ children.each { |child| child.thaw_data_from(storage) }
42
+ end
43
+
44
+
45
+ # Serializes the widget node structure (not children, not content).
46
+ def dump_node
47
+ field_sep = self.class.field_sep
48
+ "#{@name}#{field_sep}#{self.class}#{field_sep}#{root? ? @name : parent.name}"
49
+ end
50
+
51
+ # Serializes the tree structure.
52
+ def _dump(depth)
53
+ inject("") { |str, node| str << node.dump_node << self.class.node_sep }
54
+ end
55
+
56
+ module ClassMethods
57
+ def field_sep; '|'; end
58
+ def node_sep; "\n"; end
59
+
60
+ # Creates an empty widget instance from <tt>line</tt>.
61
+ def load_node(line)
62
+ name, klass, parent = line.split(field_sep)
63
+ [klass.constantize.new(name, nil), parent]
64
+ end
65
+
66
+ def _load(str)
67
+ nodes = {}
68
+ root = nil
69
+ str.split(node_sep).each do |line|
70
+ node, parent = load_node(line)
71
+ nodes[node.name] = node
72
+
73
+ if node.name == parent # we're at the root node.
74
+ root = node and next
75
+ end
76
+
77
+ nodes[parent].add(node)
78
+ end
79
+ root
80
+ end
81
+
82
+ def freeze_for(storage, root)
83
+ storage[:apotomo_stateful_branches] = []
84
+ storage[:apotomo_widget_ivars] = {}
85
+
86
+ stateful_branches_for(root).each do |branch|
87
+ branch.freeze_data_to(storage[:apotomo_widget_ivars]) # save ivars.
88
+ storage[:apotomo_stateful_branches] << [branch, branch.parent.name]
89
+ branch.root! # disconnect from tree.
90
+ end
91
+ end
92
+
93
+ def thaw_for(storage, root)
94
+ branches = storage.delete(:apotomo_stateful_branches) || []
95
+ branches.each do |config|
96
+ branch = config.first
97
+ parent = root.find_widget(config.last) or raise "Couldn't find parent `#{config.last}` for `#{branch.name}`"
98
+
99
+ parent << branch
100
+ branch.thaw_data_from(storage.delete(:apotomo_widget_ivars) || {})
101
+ end
102
+
103
+ root
104
+ end
105
+
106
+ def thaw_from(storage)
107
+ root = storage[:apotomo_root]
108
+ root.thaw_data_from(storage.fetch(:apotomo_widget_ivars, {}))
109
+ root
110
+ end
111
+
112
+ def frozen_widget_in?(storage)
113
+ branches = storage[:apotomo_stateful_branches]
114
+ branches.present? and branches.first.first.kind_of? Apotomo::StatefulWidget
115
+ end
116
+
117
+ def flush_storage(storage)
118
+ storage[:apotomo_stateful_branches] = nil
119
+ storage[:apotomo_widget_ivars] = nil
120
+ end
121
+
122
+ # Find the first stateful widgets on each branch from +root+.
123
+ def stateful_branches_for(root)
124
+ to_traverse = [root]
125
+ stateful_roots = []
126
+
127
+ while node = to_traverse.shift
128
+ if node.kind_of?(StatefulWidget)
129
+ stateful_roots << node and next
130
+ end
131
+ to_traverse += node.children
132
+ end
133
+
134
+ stateful_roots
135
+ end
136
+
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,18 @@
1
+ module Apotomo
2
+ class ProcEventHandler < EventHandler
3
+ attr_accessor :proc
4
+
5
+ def initialize(opts={})
6
+ @proc = opts.delete(:proc)
7
+ end
8
+
9
+ def process_event(event)
10
+ Rails.logger.debug "ProcEventHandler: calling #{@proc}"
11
+ #@proc.call(event)
12
+ event.source.controller.send(@proc, event)
13
+ nil ### DISCUSS: needed so that controller doesn't evaluate the "content".
14
+ end
15
+
16
+ def to_s; "ProcEventHandler:#{proc}"; end
17
+ end
18
+ end
@@ -0,0 +1,161 @@
1
+ require 'apotomo/request_processor'
2
+ require 'apotomo/rails/view_methods'
3
+
4
+ module Apotomo
5
+ module Rails
6
+ module ControllerMethods
7
+ include WidgetShortcuts
8
+
9
+ def self.included(base) #:nodoc:
10
+ base.class_eval do
11
+ extend WidgetShortcuts
12
+ extend ClassMethods
13
+
14
+ class_inheritable_array :uses_widgets_blocks
15
+ self.uses_widgets_blocks = []
16
+
17
+ helper ::Apotomo::Rails::ViewMethods
18
+
19
+ after_filter :apotomo_freeze
20
+ end
21
+ end
22
+
23
+ module ClassMethods
24
+ def uses_widgets(&block)
25
+ uses_widgets_blocks << block
26
+ end
27
+
28
+ alias_method :has_widgets, :uses_widgets
29
+ end
30
+
31
+ def bound_use_widgets_blocks
32
+ session[:bound_use_widgets_blocks] ||= ProcHash.new
33
+ end
34
+
35
+ def flush_bound_use_widgets_blocks
36
+ session[:bound_use_widgets_blocks] = nil
37
+ end
38
+
39
+ def apotomo_request_processor
40
+ return @apotomo_request_processor if @apotomo_request_processor
41
+
42
+ # happens once per request:
43
+ ### DISCUSS: policy in production?
44
+ options = { :flush_widgets => params[:flush_widgets],
45
+ :js_framework => Apotomo.js_framework || :prototype,
46
+ } ### TODO: process rails options (flush_tree, version)
47
+
48
+ @apotomo_request_processor = Apotomo::RequestProcessor.new(session, options, self.class.uses_widgets_blocks)
49
+
50
+ flush_bound_use_widgets_blocks if @apotomo_request_processor.widgets_flushed?
51
+
52
+
53
+ @apotomo_request_processor
54
+ end
55
+
56
+ def apotomo_root
57
+ apotomo_request_processor.root
58
+ end
59
+
60
+ # Yields the root widget for manipulating the widget tree in a controller action.
61
+ # Note that the passed block is executed once per session and not in every request.
62
+ #
63
+ # Example:
64
+ # def login
65
+ # use_widgets do |root|
66
+ # root << cell(:login, :form, 'login_box')
67
+ # end
68
+ #
69
+ # @box = render_widget 'login_box'
70
+ # end
71
+ def use_widgets(&block)
72
+ root = apotomo_root ### DISCUSS: let RequestProcessor initialize so we get flushed, eventually. maybe add a :before filter for that? or move #use_widgets to RequestProcessor?
73
+
74
+ return if bound_use_widgets_blocks.include?(block)
75
+
76
+ yield root
77
+
78
+ bound_use_widgets_blocks << block # remember the proc.
79
+ end
80
+
81
+
82
+ def render_widget(widget, options={}, &block)
83
+ apotomo_request_processor.render_widget_for(widget, options, self, &block)
84
+ end
85
+
86
+ def apotomo_freeze
87
+ apotomo_request_processor.freeze!
88
+ end
89
+
90
+ def render_event_response
91
+ page_updates = apotomo_request_processor.process_for({:type => params[:type], :source => params[:source]}, self)
92
+
93
+ return render_iframe_updates(page_updates) if params[:apotomo_iframe]
94
+
95
+ render :text => apotomo_request_processor.render_page_updates(page_updates), :content_type => Mime::JS
96
+ end
97
+
98
+ # Returns the url to trigger a +type+ event from +:source+, which is a non-optional parameter.
99
+ # Additional +options+ will be appended to the query string.
100
+ #
101
+ # Note that this method will use the framework's internal routing if available (e.g. #url_for in Rails).
102
+ #
103
+ # Example:
104
+ # url_for_event(:paginate, :source => 'mouse', :page => 2)
105
+ # #=> http://apotomo.de/mouse/process_event_request?type=paginate&source=mouse&page=2
106
+ def url_for_event(type, options)
107
+ options.reverse_merge!(:type => type)
108
+
109
+ apotomo_event_path(apotomo_request_processor.address_for(options))
110
+ end
111
+
112
+ protected
113
+
114
+
115
+ # Renders the page updates through an iframe. Copied from responds_to_parent,
116
+ # see http://github.com/markcatley/responds_to_parent .
117
+ def render_iframe_updates(page_updates)
118
+ script = apotomo_request_processor.render_page_updates(page_updates)
119
+ escaped_script = Apotomo::JavascriptGenerator.escape(script)
120
+
121
+ render :text => "<html><body><script type='text/javascript' charset='utf-8'>
122
+ var loc = document.location;
123
+ with(window.parent) { setTimeout(function() { window.eval('#{escaped_script}'); window.loc && loc.replace('about:blank'); }, 1) }
124
+ </script></body></html>", :content_type => 'text/html'
125
+ end
126
+
127
+ def respond_to_event(type, options)
128
+ handler = ProcEventHandler.new
129
+ handler.proc = options[:with]
130
+ ### TODO: pass :from => (event source).
131
+
132
+ # attach once, not every request:
133
+ apotomo_root.evt_table.add_handler_once(handler, :event_type => type)
134
+ end
135
+
136
+
137
+
138
+ ### DISCUSS: rename? should say "this controller action wants apotomo's deep linking!"
139
+ ### DISCUSS: move to deep_link_methods?
140
+ def respond_to_url_change
141
+ return if apotomo_root.find_widget('deep_link') # add only once.
142
+ apotomo_root << widget("apotomo/deep_link_widget", :setup, 'deep_link')
143
+ end
144
+
145
+
146
+ class ProcHash < Array
147
+ def id_for_proc(proc)
148
+ proc.to_s.split('@').last
149
+ end
150
+
151
+ def <<(proc)
152
+ super(id_for_proc(proc))
153
+ end
154
+
155
+ def include?(proc)
156
+ super(id_for_proc(proc))
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end