skozlov-netzke-core 0.1.0.2 → 0.4.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 (48) hide show
  1. data/CHANGELOG +108 -0
  2. data/LICENSE +2 -19
  3. data/Manifest +50 -0
  4. data/README.rdoc +12 -0
  5. data/Rakefile +2 -3
  6. data/TODO +2 -0
  7. data/autotest/discover.rb +3 -0
  8. data/generators/netzke_core/netzke_core_generator.rb +6 -6
  9. data/generators/netzke_core/templates/create_netzke_preferences.rb +2 -2
  10. data/javascripts/core.js +474 -111
  11. data/lib/app/controllers/netzke_controller.rb +76 -0
  12. data/lib/app/models/netzke_preference.rb +128 -39
  13. data/lib/netzke-core.rb +23 -9
  14. data/lib/netzke/action_view_ext.rb +26 -0
  15. data/lib/netzke/base.rb +440 -102
  16. data/lib/netzke/base_js.rb +258 -0
  17. data/lib/netzke/controller_extensions.rb +80 -29
  18. data/lib/netzke/core_ext.rb +72 -21
  19. data/lib/netzke/feedback_ghost.rb +43 -0
  20. data/lib/netzke/routing.rb +9 -0
  21. data/netzke-core.gemspec +10 -11
  22. data/stylesheets/core.css +12 -0
  23. data/test/app_root/app/controllers/{application.rb → application_controller.rb} +0 -0
  24. data/test/app_root/app/models/role.rb +3 -0
  25. data/test/app_root/app/models/user.rb +3 -0
  26. data/test/app_root/config/boot.rb +2 -1
  27. data/test/app_root/config/database.yml +10 -0
  28. data/test/app_root/config/environment.rb +1 -0
  29. data/test/app_root/db/migrate/20081222035855_create_netzke_preferences.rb +18 -0
  30. data/test/app_root/db/migrate/20090423214303_create_roles.rb +11 -0
  31. data/test/app_root/db/migrate/20090423222114_create_users.rb +12 -0
  32. data/test/app_root/lib/console_with_fixtures.rb +4 -0
  33. data/test/app_root/script/console +1 -0
  34. data/test/fixtures/roles.yml +7 -0
  35. data/test/fixtures/users.yml +9 -0
  36. data/test/test_helper.rb +3 -2
  37. data/test/unit/core_ext_test.rb +66 -0
  38. data/test/unit/netzke_core_test.rb +167 -0
  39. data/test/unit/netzke_preference_test.rb +103 -0
  40. metadata +45 -30
  41. data/README.mdown +0 -11
  42. data/generators/netzke_core/templates/create_netzke_layouts.rb +0 -14
  43. data/generators/netzke_core/templates/netzke.html.erb +0 -10
  44. data/lib/app/models/netzke_layout.rb +0 -75
  45. data/lib/netzke/js_class_builder.rb +0 -114
  46. data/lib/vendor/facets/hash/recursive_merge.rb +0 -28
  47. data/test/core_ext_test.rb +0 -35
  48. data/test/netzke_core_test.rb +0 -136
@@ -0,0 +1,258 @@
1
+ module Netzke
2
+ # == BaseJs
3
+ # *TODO: outdated*
4
+ #
5
+ # Module which provides JS-class generation functionality for the widgets ("client-side"). The generated code
6
+ # is evaluated once per widget class, and the results are cached in the browser. Included into Netzke::Base class.
7
+ #
8
+ # == Widget javascript code
9
+ # Here's a brief explanation on how a javascript class for a widget gets built.
10
+ # Widget gets defined as a constructor (a function) by +js_class+ class method (see "Inside widget's contstructor").
11
+ # +Ext.extend+ provides inheritance from an Ext class specified in +js_base_class+ class method.
12
+ #
13
+ # == Inside widget's constructor
14
+ # * Widget's constructor gets called with a parameter that is a configuration object provided by +js_config+ instance method. This configuration is specific for the instance of the widget, and, for example, contains this widget's unique id. As another example, by means of this configuration object, a grid receives the configuration array for its columns, a form - for its fields, etc. With other words, everything that may change from instance to instance of the same widget's class, goes in here.
15
+ # * Widget executes its specific initialization code which is provided by +js_before_consttructor+ class method.
16
+ # For example, a grid may define its column model, a form - its fields, a tab panel - its tabs ("items").
17
+ # * Widget calls the constructor of the inherited class (see +js_class+ class method) with a parameter that is a merge of
18
+ # 1) configuration parameter passed to the widget's constructor.
19
+ module BaseJs
20
+ def self.included(base)
21
+ base.extend ClassMethods
22
+ end
23
+
24
+ #
25
+ # Instance methods
26
+ #
27
+
28
+ # Config that is used for instantiating the widget in javascript
29
+ def js_config
30
+ res = {}
31
+
32
+ # Unique id of the widget
33
+ res.merge!(:id => id_name)
34
+
35
+ # Recursively include configs of all non-late aggregatees, so that the widget can instantiate them
36
+ # in javascript immediately.
37
+ non_late_aggregatees.each_pair do |aggr_name, aggr_config|
38
+ aggr_instance = aggregatee_instance(aggr_name.to_sym)
39
+ aggr_instance.before_load
40
+ res["#{aggr_name}_config".to_sym] = aggr_instance.js_config
41
+ end
42
+
43
+ # Api (besides the default "load_aggregatee_with_cache" - JavaScript side already knows about it)
44
+ res.merge!(:api => self.class.api_points.reject{ |p| p == :load_aggregatee_with_cache })
45
+
46
+ # Widget class name. Needed for dynamic instantiation in javascript.
47
+ res.merge!(:widget_class_name => short_widget_class_name)
48
+
49
+ # Actions, toolbars and menus
50
+ # tools && res.merge!(:tools => tools)
51
+ actions && res.merge!(:actions => actions)
52
+ menu && res.merge!(:menu => menu)
53
+
54
+ # Merge with all config options passed as hash to config[:ext_config]
55
+ res.merge!(ext_config)
56
+
57
+ res
58
+ end
59
+
60
+ # All the JS-code required by this instance of the widget to be instantiated in the browser.
61
+ # It includes the JS-class for the widget itself, as well as JS-classes for all widgets' (non-late) aggregatees.
62
+ def js_missing_code(cached_dependencies = [])
63
+ code = dependency_classes.inject("") do |r,k|
64
+ cached_dependencies.include?(k) ? r : r + "Netzke::#{k}".constantize.js_code(cached_dependencies).strip_js_comments
65
+ end
66
+ code.blank? ? nil : code
67
+ end
68
+
69
+ def css_missing_code(cached_dependencies = [])
70
+ code = dependency_classes.inject("") do |r,k|
71
+ cached_dependencies.include?(k) ? r : r + "Netzke::#{k}".constantize.css_code(cached_dependencies)
72
+ end
73
+ code.blank? ? nil : code
74
+ end
75
+
76
+ #
77
+ # The following methods are used when a widget is generated stand-alone (as a part of a HTML page)
78
+ #
79
+
80
+ # instantiating
81
+ def js_widget_instance
82
+ %Q{var #{name.jsonify} = new Ext.netzke.cache.#{short_widget_class_name}(#{js_config.to_nifty_json});}
83
+ end
84
+
85
+ # rendering
86
+ def js_widget_render
87
+ %Q{#{name.jsonify}.render("#{name.to_s.split('_').join('-')}");}
88
+ end
89
+
90
+ # container for rendering
91
+ def js_widget_html
92
+ %Q{<div id="#{name.to_s.split('_').join('-')}"></div>}
93
+ end
94
+
95
+ #
96
+ #
97
+ #
98
+
99
+ # Widget's actions, tools and menus that are loaded at the moment of instantiation
100
+ def actions; nil; end
101
+ def menu; nil; end
102
+ # def tools; nil; end
103
+
104
+ # Little helpers
105
+ def this; "this".l; end
106
+ def null; "null".l; end
107
+
108
+ # Methods used to create the javascript class (only once per widget class).
109
+ # The generated code gets cached at the browser, and the widget intstances (at the browser side)
110
+ # get instantiated from it.
111
+ # All these methods can be overwritten in case you want to extend the functionality of some pre-built widget
112
+ # instead of using it as is (using both would cause JS-code duplication)
113
+ module ClassMethods
114
+ # the JS (Ext) class that we inherit from on JS-level
115
+ def js_base_class
116
+ "Ext.Panel"
117
+ end
118
+
119
+ # functions and properties that will be used to extend the functionality of (Ext) JS-class specified in js_base_class
120
+ def js_extend_properties
121
+ {}
122
+ end
123
+
124
+ # widget's menus
125
+ def js_menus; []; end
126
+
127
+ # items
128
+ # def js_items; null; end
129
+
130
+ # are we using JS inheritance? for now, if js_base_class is a Netzke class - yes
131
+ def js_inheritance?
132
+ superclass != Netzke::Base
133
+ end
134
+
135
+ # Declaration of widget's class (stored in the cache storage (Ext.netzke.cache) at the client side
136
+ # to be reused at the moment of widget instantiation)
137
+ def js_class
138
+ if js_inheritance?
139
+ # In case of using javascript inheritance, little needs to be done
140
+ <<-END_OF_JAVASCRIPT
141
+ // Create the class
142
+ Ext.netzke.cache.#{short_widget_class_name} = function(config){
143
+ Ext.netzke.cache.#{short_widget_class_name}.superclass.constructor.call(this, config);
144
+ };
145
+ // Extend it with the class that we inherit from, and mix in js_extend_properties
146
+ Ext.extend(Ext.netzke.cache.#{short_widget_class_name}, Ext.netzke.cache.#{superclass.short_widget_class_name}, Ext.applyIf(#{js_extend_properties.to_nifty_json}, Ext.widgetMixIn));
147
+ END_OF_JAVASCRIPT
148
+ else
149
+ js_add_menus = "this.addMenus(#{js_menus.to_nifty_json});" unless js_menus.empty?
150
+ <<-END_OF_JAVASCRIPT
151
+ // Constructor
152
+ Ext.netzke.cache.#{short_widget_class_name} = function(config){
153
+ // Do all the initializations that every Netzke widget should do: create methods for API-points,
154
+ // process actions, tools, toolbars
155
+ this.commonBeforeConstructor(config);
156
+
157
+ // Call the constructor of the inherited class
158
+ Ext.netzke.cache.#{short_widget_class_name}.superclass.constructor.call(this, config);
159
+
160
+ // What every widget should do after calling the constructor of the inherited class, like
161
+ // setting extra events
162
+ this.commonAfterConstructor(config);
163
+ };
164
+ Ext.extend(Ext.netzke.cache.#{short_widget_class_name}, #{js_base_class}, Ext.applyIf(#{js_extend_properties.to_nifty_json}, Ext.widgetMixIn));
165
+ END_OF_JAVASCRIPT
166
+ end
167
+ end
168
+
169
+ def css_include(*args)
170
+ included_css = read_inheritable_attribute(:included_css) || []
171
+ args.each do |inclusion|
172
+ if inclusion.is_a?(Hash)
173
+ # we are signalized a non-default file location (e.g. Ext examples)
174
+ case inclusion.keys.first
175
+ when :ext_examples
176
+ location = Netzke::Base.config[:ext_location] + "/examples/"
177
+ end
178
+ files = inclusion.values.first
179
+ else
180
+ location = ""
181
+ files = inclusion
182
+ end
183
+
184
+ files = [files] if files.is_a?(String)
185
+
186
+ for f in files
187
+ included_css << location + f
188
+ end
189
+ end
190
+ write_inheritable_attribute(:included_css, included_css)
191
+ end
192
+
193
+ # all JS code needed for this class, including one from the ancestor widget
194
+ def js_code(cached_dependencies = [])
195
+ res = ""
196
+
197
+ # include the base-class javascript if doing JS inheritance
198
+ res << superclass.js_code << "\n" if js_inheritance? && !cached_dependencies.include?(superclass.short_widget_class_name)
199
+
200
+ # include static javascripts
201
+ res << js_included << "\n"
202
+
203
+ # our own JS class definition
204
+ res << js_class
205
+ res
206
+ end
207
+
208
+
209
+ # Override this method. Must return an array of paths to javascript files that we depend on.
210
+ # This javascript code will be loaded along with the widget's class, and before it.
211
+ def include_js
212
+ []
213
+ end
214
+
215
+ # returns all extra js-code (as string) required by this widget's class
216
+ def js_included
217
+ res = ""
218
+
219
+ include_js.each do |path|
220
+ f = File.new(path)
221
+ res << f.read << "\n"
222
+ end
223
+
224
+ res
225
+ end
226
+
227
+
228
+ # returns all extra js-code (as string) required by this widget's class
229
+ def css_included
230
+ res = ""
231
+
232
+ included_css = read_inheritable_attribute(:included_css) || []
233
+ res << included_css.inject("") do |r, path|
234
+ f = File.new(path)
235
+ r << f.read
236
+ end
237
+
238
+ res
239
+ end
240
+
241
+ # all JS code needed for this class including the one from the ancestor widget
242
+ def css_code(cached_dependencies = [])
243
+ res = ""
244
+
245
+ # include the base-class javascript if doing JS inheritance
246
+ res << superclass.css_code << "\n" if js_inheritance? && !cached_dependencies.include?(superclass.short_widget_class_name)
247
+
248
+ res << css_included << "\n"
249
+
250
+ res
251
+ end
252
+
253
+ def this; "this".l; end
254
+ def null; "null".l; end
255
+
256
+ end
257
+ end
258
+ end
@@ -2,6 +2,29 @@ module Netzke
2
2
  module ControllerExtensions
3
3
  def self.included(base)
4
4
  base.extend ControllerClassMethods
5
+ base.send(:before_filter, :set_session_data)
6
+ end
7
+
8
+ def set_session_data
9
+ Netzke::Base.session = session
10
+ session[:netzke_user_id] = defined?(current_user) ? current_user.try(:id) : nil
11
+
12
+ Netzke::Base.user = current_user # for backward compatibility (TODO: eliminate the need for this)
13
+
14
+ # set netzke_just_logged_in and netzke_just_logged_out states (may be used by Netzke widgets)
15
+ if session[:_netzke_next_request_is_first_after_login]
16
+ session[:netzke_just_logged_in] = true
17
+ session[:_netzke_next_request_is_first_after_login] = false
18
+ else
19
+ session[:netzke_just_logged_in] = false
20
+ end
21
+
22
+ if session[:_netzke_next_request_is_first_after_logout]
23
+ session[:netzke_just_logged_out] = true
24
+ session[:_netzke_next_request_is_first_after_logout] = false
25
+ else
26
+ session[:netzke_just_logged_out] = false
27
+ end
5
28
  end
6
29
 
7
30
  def method_missing(method_name)
@@ -12,16 +35,19 @@ module Netzke
12
35
  widget = widget.to_sym
13
36
  action = !action.empty? && action.join("__").to_sym
14
37
 
15
- # only widget's actions starting with "interface_" are accessible from outside (security)
38
+ # only widget's actions starting with "api_" are accessible from outside (security)
16
39
  if action
17
- interface_action = action.to_s.index('__') ? action : "interface_#{action}"
40
+ api_action = action.to_s.index('__') ? action : "api_#{action}"
18
41
 
19
42
  # widget module
20
43
  widget_class = "Netzke::#{self.class.widget_config_storage[widget][:widget_class_name]}".constantize
21
44
 
22
45
  # instantiate the server part of the widget
23
- widget_instance = widget_class.new(self.class.widget_config_storage[widget].merge(:controller => self)) # OPTIMIZE: top-level widgets have access to the controller - can we avoid that?
24
- render :text => widget_instance.send(interface_action, params)
46
+ widget_instance = widget_class.new(self.class.widget_config_storage[widget])
47
+ # (OLD VERSION)
48
+ # widget_instance = widget_class.new(self.class.widget_config_storage[widget].merge(:controller => self)) # OPTIMIZE: top-level widgets have access to the controller - can we avoid that?
49
+
50
+ render :text => widget_instance.send(api_action, params)
25
51
  end
26
52
  end
27
53
  end
@@ -31,12 +57,13 @@ module Netzke
31
57
  # widget_config_storage for all widgets
32
58
  def widget_config_storage
33
59
  @@widget_config_storage ||= {}
60
+ @@widget_config_storage[self.name] ||= {} # specific for controller
34
61
  end
35
62
 
36
63
  #
37
64
  # Use this method to declare a widget in the controller
38
65
  #
39
- def netzke_widget(name, config={})
66
+ def netzke(name, config={})
40
67
  # which module is the widget?
41
68
  config[:widget_class_name] ||= name.to_s.classify
42
69
  config[:name] ||= name
@@ -46,39 +73,63 @@ module Netzke
46
73
 
47
74
  # provide widget helpers
48
75
  ActionView::Base.module_eval <<-END_EVAL, __FILE__, __LINE__
76
+ def #{name}_server_instance(config = {})
77
+ default_config = controller.class.widget_config_storage[:#{name}]
78
+ if config.empty?
79
+ # only cache when the config is empty (which means that config specified in controller is used)
80
+ @widget_instance_cache ||= {}
81
+ @widget_instance_cache[:#{name}] ||= Netzke::Base.instance_by_config(default_config)
82
+ else
83
+ # if helper is called with parameters - always return a fresh instance of widget, no caching
84
+ Netzke::Base.instance_by_config(default_config.deep_merge(config))
85
+ end
86
+ end
87
+
49
88
  def #{name}_widget_instance(config = {})
50
- # get the global config from the controller's singleton class
51
- global_config = controller.class.widget_config_storage[:#{name}]
52
-
53
- # when instantiating a client side instance, the configuration may be overwritten
54
- # (but the server side will know nothing about it!)
55
- local_config = global_config.merge(config)
56
-
57
- # instantiate it
58
- widget_instance = Netzke::#{config[:widget_class_name]}.new(local_config)
59
-
60
- # return javascript code for instantiating on the javascript level
61
- widget_instance.js_widget_instance
89
+ #{name}_server_instance(config).js_widget_instance
62
90
  end
63
91
 
64
92
  def #{name}_class_definition
65
- config = controller.class.widget_config_storage[:#{name}]
66
- Netzke::#{config[:widget_class_name]}.new(config).js_missing_code
93
+ @generated_widget_classes ||= []
94
+ res = #{name}_server_instance.js_missing_code(@generated_widget_classes)
95
+
96
+ # prevent duplication of javascript when multiple homogeneous widgets are on the same page
97
+ @generated_widget_classes += #{name}_server_instance.dependencies
98
+ @generated_widget_classes.uniq!
99
+ res
67
100
  end
101
+
102
+ def #{name}_widget_html
103
+ #{name}_server_instance.js_widget_html
104
+ end
105
+
106
+ def #{name}_widget_render
107
+ #{name}_server_instance.js_widget_render
108
+ end
109
+
68
110
  END_EVAL
69
111
 
70
112
  # add controller action which will render a simple HTML page containing the widget
71
113
  define_method("#{name}_test") do
72
- render :inline => %Q(
73
- <script type="text/javascript" charset="utf-8">
74
- <%= #{name}_class_definition %>
75
- Ext.onReady(function(){
76
- <%= #{name}_widget_instance %>
77
- #{name.to_js}.render("#{name.to_s.split('_').join('-')}");
78
- })
79
- </script>
80
- <div id="#{name.to_s.split('_').join('-')}"></div>
81
- ), :layout => "netzke"
114
+ @widget_name = name
115
+ render :inline => <<-HTML
116
+ <head>
117
+ <meta http-equiv="Content-type" content="text/html; charset=utf-8">
118
+ <title><%= @widget_name %></title>
119
+ <%= netzke_js_include %>
120
+ <%= netzke_css_include %>
121
+ <script type="text/javascript" charset="utf-8">
122
+ <%= #{name}_class_definition %>
123
+ Ext.onReady(function(){
124
+ <%= #{name}_widget_instance %>
125
+ <%= #{name}_widget_render %>
126
+ });
127
+ </script>
128
+ </head>
129
+ <body>
130
+ <%= #{name}_widget_html %>
131
+ </body>
132
+ HTML
82
133
  end
83
134
  end
84
135
  end
@@ -9,18 +9,29 @@ class Hash
9
9
  h
10
10
  end : self
11
11
  end
12
+
13
+ def jsonify
14
+ self.inject({}) do |h,(k,v)|
15
+ new_key = k.instance_of?(String) || k.instance_of?(Symbol) ? k.jsonify : k
16
+ new_value = v.instance_of?(Array) || v.instance_of?(Hash) ? v.jsonify : v
17
+ h.merge(new_key => new_value)
18
+ end
19
+ end
12
20
 
13
21
  # First camelizes the keys, then convert the whole hash to JSON
14
- def to_js
15
- # self.delete_if{ |k,v| v.nil? } # we don't need to explicitely pass null values to javascript
16
- self.recursive_delete_if_nil.convert_keys{|k| k.camelize(:lower)}.to_json
17
- end
18
-
19
- # Converts values to strings
20
- def stringify_values!
21
- self.each_pair{|k,v| self[k] = v.to_s if v.is_a?(Symbol)}
22
+ def to_nifty_json
23
+ self.recursive_delete_if_nil.jsonify.to_json
22
24
  end
23
25
 
26
+ # Converts values of a Hash in such a way that they can be easily stored in the database: hashes and arrays are jsonified, symbols - stringified
27
+ def deebeefy_values
28
+ inject({}) do |options, (k, v)|
29
+ options[k] = v.is_a?(Symbol) ? v.to_s : (v.is_a?(Hash) || v.is_a?(Array)) ? v.to_json : v
30
+ options
31
+ end
32
+ end
33
+
34
+ # We don't need to pass null values in JSON, they are null by simply being absent
24
35
  def recursive_delete_if_nil
25
36
  self.inject({}) do |h,(k,v)|
26
37
  if !v.nil?
@@ -29,13 +40,29 @@ class Hash
29
40
  h
30
41
  end
31
42
  end
43
+
44
+ # Javascrit-like access to Hash values
45
+ def method_missing(method, *args)
46
+ if method.to_s =~ /=$/
47
+ method_base = method.to_s.sub(/=$/,'').to_sym
48
+ key = self[method_base.to_s].nil? ? method_base : method_base.to_s
49
+ self[key] = args.first
50
+ else
51
+ key = self[method.to_s].nil? ? method : method.to_s
52
+ self[key]
53
+ end
54
+ end
32
55
 
33
56
  end
34
57
 
35
58
  class Array
59
+ def jsonify
60
+ self.map{ |el| el.instance_of?(Array) || el.instance_of?(Hash) ? el.jsonify : el }
61
+ end
62
+
36
63
  # Camelizes the keys of hashes and converts them to JSON
37
- def to_js
38
- self.recursive_delete_if_nil.map{|el| el.is_a?(Hash) ? el.convert_keys{|k| k.camelize(:lower)} : el}.to_json
64
+ def to_nifty_json
65
+ self.recursive_delete_if_nil.jsonify.to_json
39
66
  end
40
67
 
41
68
  # Applies convert_keys to each element which responds to convert_keys
@@ -50,28 +77,52 @@ class Array
50
77
  end
51
78
  end
52
79
 
53
- class String
54
- # Converts self to "literal JSON"-string - one that doesn't get quotes appended when being sent "to_json" method
55
- def l
56
- def self.to_json
57
- self
58
- end
80
+ class LiteralString < String
81
+ def to_json(*args)
59
82
  self
60
83
  end
61
-
62
- def to_js
84
+ end
85
+
86
+ class String
87
+ def jsonify
63
88
  self.camelize(:lower)
64
89
  end
65
90
 
66
- # removes JS-comments (both single-line and multiple-line) from the string
91
+ # Converts self to "literal JSON"-string - one that doesn't get quotes appended when being sent "to_json" method
92
+ def l
93
+ LiteralString.new(self)
94
+ end
95
+
96
+ # removes JS-comments (both single- and multi-line) from the string
67
97
  def strip_js_comments
68
98
  regexp = /\/\/.*$|(?m:\/\*.*?\*\/)/
69
- self.gsub(regexp, '')
99
+ self.gsub!(regexp, '')
100
+
101
+ # also remove empty lines
102
+ regexp = /^\s*\n/
103
+ self.gsub!(regexp, '')
104
+ end
105
+
106
+ # "false" => false, "whatever_else" => true
107
+ def to_b
108
+ self != "false"
70
109
  end
71
110
  end
72
111
 
73
112
  class Symbol
74
- def to_js
113
+ def jsonify
75
114
  self.to_s.camelize(:lower).to_sym
76
115
  end
116
+
117
+ def l
118
+ LiteralString.new(self.to_s)
119
+ end
120
+ end
121
+
122
+ module ActiveSupport
123
+ class TimeWithZone
124
+ def to_json(options = {})
125
+ self.to_s(:db).to_json
126
+ end
127
+ end
77
128
  end