masterview 0.2.5 → 0.3.0

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 (155) hide show
  1. data/CHANGELOG +31 -1
  2. data/README +70 -69
  3. data/RELEASE_NOTES +70 -64
  4. data/Rakefile +26 -27
  5. data/TODO +13 -29
  6. data/doc/about.html +246 -0
  7. data/doc/configuration.html +49 -36
  8. data/doc/developer.html +423 -41
  9. data/doc/directives.html +139 -51
  10. data/doc/guide.html +19 -9
  11. data/doc/index.html +90 -224
  12. data/doc/installation.html +36 -28
  13. data/doc/media_list.html +30 -20
  14. data/doc/simple_diagram.html +3 -5
  15. data/doc/stylesheets/masterview.css +16 -1
  16. data/examples/rails_app_config/masterview/settings.rb +2 -1
  17. data/init.rb +1 -1
  18. data/lib/#ChangeLog# +6 -0
  19. data/lib/masterview/analyzer.rb +48 -34
  20. data/lib/masterview/attr_string_parser.rb +5 -1
  21. data/lib/masterview/case_insensitive_hash.rb +69 -0
  22. data/lib/masterview/{pathname_extensions.rb → core_ext/pathname.rb} +0 -0
  23. data/lib/masterview/{string_extensions.rb → core_ext/string.rb} +0 -0
  24. data/lib/masterview/deprecated/directive_base.rb +362 -0
  25. data/lib/masterview/directive_base.rb +201 -179
  26. data/lib/masterview/directive_dsl.rb +457 -0
  27. data/lib/masterview/directive_helpers.rb +28 -141
  28. data/lib/masterview/directive_load_path.rb +388 -0
  29. data/lib/masterview/directive_metadata.rb +377 -0
  30. data/lib/masterview/directive_registry.rb +259 -69
  31. data/lib/masterview/directives/.metadata +16 -0
  32. data/lib/masterview/directives/attr.rb +9 -8
  33. data/lib/masterview/directives/block.rb +11 -14
  34. data/lib/masterview/directives/check_box.rb +13 -18
  35. data/lib/masterview/directives/collection_select.rb +15 -29
  36. data/lib/masterview/directives/content.rb +9 -3
  37. data/lib/masterview/directives/else.rb +15 -13
  38. data/lib/masterview/directives/elsif.rb +14 -13
  39. data/lib/masterview/directives/eval.rb +20 -0
  40. data/lib/masterview/directives/form.rb +56 -9
  41. data/lib/masterview/directives/form_remote.rb +26 -0
  42. data/lib/masterview/directives/global_inline_erb.rb +10 -14
  43. data/lib/masterview/directives/hidden_field.rb +11 -20
  44. data/lib/masterview/directives/if.rb +13 -12
  45. data/lib/masterview/directives/image_tag.rb +20 -28
  46. data/lib/masterview/directives/import.rb +5 -12
  47. data/lib/masterview/directives/import_render.rb +7 -19
  48. data/lib/masterview/directives/insert_generated_comment.rb +8 -11
  49. data/lib/masterview/directives/javascript_include.rb +21 -12
  50. data/lib/masterview/directives/link_to.rb +14 -8
  51. data/lib/masterview/directives/link_to_function.rb +22 -0
  52. data/lib/masterview/directives/link_to_if.rb +15 -13
  53. data/lib/masterview/directives/link_to_remote.rb +13 -8
  54. data/lib/masterview/directives/omit_tag.rb +32 -16
  55. data/lib/masterview/directives/password_field.rb +10 -22
  56. data/lib/masterview/directives/radio_button.rb +11 -22
  57. data/lib/masterview/directives/replace.rb +7 -8
  58. data/lib/masterview/directives/select.rb +11 -24
  59. data/lib/masterview/directives/stylesheet_link.rb +20 -12
  60. data/lib/masterview/directives/submit.rb +11 -5
  61. data/lib/masterview/directives/text_area.rb +10 -23
  62. data/lib/masterview/directives/text_field.rb +10 -22
  63. data/lib/masterview/exceptions.rb +21 -0
  64. data/lib/masterview/extras/app/controllers/masterview_controller.rb +102 -75
  65. data/lib/masterview/extras/app/views/layouts/masterview_admin.rhtml +24 -23
  66. data/lib/masterview/extras/app/views/layouts/masterview_admin_config.rhtml +81 -0
  67. data/lib/masterview/extras/app/views/masterview/admin/configuration.rhtml +5 -1
  68. data/lib/masterview/extras/app/views/masterview/admin/create.rhtml +2 -2
  69. data/lib/masterview/extras/app/views/masterview/admin/directives.rhtml +5 -0
  70. data/lib/masterview/extras/app/views/masterview/admin/features.rhtml +5 -79
  71. data/lib/masterview/extras/app/views/masterview/admin/interact.rhtml +5 -0
  72. data/lib/masterview/extras/app/views/masterview/admin/list.rhtml +3 -71
  73. data/lib/masterview/extras/init_mv_admin_pages.rb +42 -23
  74. data/lib/masterview/filter_helpers.rb +26 -0
  75. data/lib/masterview/initializer.rb +99 -53
  76. data/lib/masterview/io.rb +19 -15
  77. data/lib/masterview/keyword_expander.rb +7 -2
  78. data/lib/masterview/masterview_info.rb +229 -23
  79. data/lib/masterview/masterview_version.rb +2 -2
  80. data/lib/masterview/parser.rb +275 -105
  81. data/lib/masterview/parser_helpers.rb +54 -0
  82. data/lib/masterview/rails_ext/action_controller_erb_direct.rb +29 -0
  83. data/lib/masterview/rails_ext/action_controller_reparse_checking.rb +27 -0
  84. data/lib/masterview/{extras/init_rails_erb_mv_direct.rb → rails_ext/action_view_erb_direct.rb} +12 -59
  85. data/lib/masterview/template_spec.rb +3 -2
  86. data/lib/masterview.rb +21 -12
  87. data/lib/rexml/parsers/baseparser_with_doctype_fix.rb +473 -0
  88. data/lib/rexml/parsers/sax2parser_with_doctype_fix.rb +243 -0
  89. data/test/directive_test_helper.rb +135 -0
  90. data/test/fixtures/directives/id_check.rb +18 -0
  91. data/test/fixtures/directives/test_directive_events.rb +70 -0
  92. data/test/test_helper.rb +18 -5
  93. data/test/tmp/views/layouts/product.rhtml +10 -10
  94. data/test/tmp/views/product/_form.rhtml +4 -4
  95. data/test/tmp/views/product/_product.rhtml +3 -3
  96. data/test/tmp/views/product/destroy.rhtml +5 -5
  97. data/test/tmp/views/product/edit.rhtml +4 -4
  98. data/test/tmp/views/product/list.rhtml +3 -3
  99. data/test/tmp/views/product/new.rhtml +4 -4
  100. data/test/tmp/views/product/show.rhtml +2 -2
  101. data/test/unit/attr_string_parser_test.rb +105 -0
  102. data/test/unit/case_insensitive_hash_mod_test.rb +104 -0
  103. data/test/unit/config_settings_test.rb +13 -1
  104. data/test/unit/default_generate_mio_filter_test.rb +3 -3
  105. data/test/unit/deprecated_directive_base_test.rb +30 -0
  106. data/test/unit/directive_attr_test.rb +111 -35
  107. data/test/unit/directive_base_test.rb +520 -1
  108. data/test/unit/directive_block_test.rb +30 -22
  109. data/test/unit/directive_content_test.rb +24 -11
  110. data/test/unit/directive_else_test.rb +18 -15
  111. data/test/unit/directive_elsif_test.rb +17 -15
  112. data/test/unit/directive_form_remote_test.rb +59 -0
  113. data/test/unit/directive_form_test.rb +31 -39
  114. data/test/unit/directive_global_inline_erb_test.rb +28 -17
  115. data/test/unit/directive_grid_test_notready.rb +38 -0
  116. data/test/unit/directive_helpers_test.rb +39 -0
  117. data/test/unit/directive_hidden_field_test.rb +44 -29
  118. data/test/unit/directive_if_test.rb +10 -7
  119. data/test/unit/directive_image_tag_test.rb +69 -61
  120. data/test/unit/directive_import_render_test.rb +28 -38
  121. data/test/unit/directive_import_test.rb +16 -14
  122. data/test/unit/directive_insert_generated_comment_test.rb +32 -0
  123. data/test/unit/directive_javascript_include_test.rb +40 -43
  124. data/test/unit/directive_link_to_function_test.rb +40 -0
  125. data/test/unit/directive_link_to_if_test.rb +52 -12
  126. data/test/unit/directive_link_to_remote_test.rb +58 -0
  127. data/test/unit/directive_link_to_test.rb +46 -31
  128. data/test/unit/directive_load_path_test.rb +257 -0
  129. data/test/unit/directive_metadata_test.rb +313 -0
  130. data/test/unit/directive_omit_tag_test.rb +73 -21
  131. data/test/unit/directive_password_field_test.rb +44 -38
  132. data/test/unit/directive_registry_test.rb +44 -0
  133. data/test/unit/directive_replace_test.rb +28 -12
  134. data/test/unit/directive_stylesheet_link_test.rb +43 -36
  135. data/test/unit/directive_submit_test.rb +29 -30
  136. data/test/unit/directive_text_area_test.rb +40 -36
  137. data/test/unit/directive_text_field_test.rb +44 -38
  138. data/test/unit/example_directive_child_events_test.rb +41 -0
  139. data/test/unit/example_test.rb +31 -4
  140. data/test/unit/file_mio_test.rb +18 -13
  141. data/test/unit/filter_helpers_test.rb +10 -8
  142. data/test/unit/find_directive_parent_test.rb +174 -0
  143. data/test/unit/keyword_expander_test.rb +4 -2
  144. data/test/unit/mio_test.rb +18 -11
  145. data/test/unit/mtime_string_hash_mio_tree_test.rb +5 -1
  146. data/test/unit/parser_test.rb +41 -29
  147. data/test/unit/pathname_extensions_test.rb +1 -1
  148. data/test/unit/run_parser_test.rb +2 -2
  149. data/test/unit/simplified_directive_base_test.rb +256 -0
  150. data/test/unit/string_hash_mio_test.rb +5 -1
  151. data/test/unit/template_file_watcher_test.rb +2 -2
  152. data/test/unit/template_test.rb +221 -46
  153. metadata +86 -45
  154. data/lib/masterview/directives/testfilter.rb +0 -55
  155. data/lib/masterview/extras/init_rails_reparse_checking.rb +0 -62
@@ -0,0 +1,362 @@
1
+ module MasterView
2
+
3
+ # Namespace module for built-in directive implementations
4
+ # that are standard with the MasterView template engine.
5
+ #
6
+ module Directives
7
+ end
8
+
9
+ # THIS CLASS IS DEPRECATED!!! UPGRADE DIRECTIVES TO USE NEW DirectiveBase
10
+ #
11
+ # Base class for directive implementations.
12
+ #
13
+ # The standard technique for implementing a directive
14
+ # is to subclass DirectiveBase. The builtin
15
+ # MasterView directives are implemented in
16
+ # module namespace MasterView::Directives.
17
+ #
18
+ # If you create a directive implementation class
19
+ # elsewhere in the class hierarchy, it is recommended
20
+ # that you include the DirectiveHelpers mixin.
21
+ # If you do not include the PluginLoadTracking mixin,
22
+ # you will need to manually register your directive
23
+ # class using the MasterView.register_directive service.
24
+ # either the PluginLoadTracking mixin or
25
+ #
26
+ #
27
+ #--
28
+ #TODO: add docs here on responsibilities and techniques for
29
+ # implementing directives. Subclass DirectiveBase, define
30
+ # within MasterView::Directives module namespace.
31
+ #
32
+ # mumble - class methods:
33
+ # attr_name is the directive's attribute markup name - def. is class name
34
+ # full_attr_name(ns) is the qualified name (name-space prefixed)
35
+ #
36
+ # Directive can optionally implement class method :on_load to initialize
37
+ # itself prior to processed for the directive registry
38
+ #
39
+ #TODO: establish protocol convention for additional namespaces
40
+ #
41
+ # mumble: :global_directive? predicate indicates... what?? (inline erb)
42
+ #
43
+ # Directives implement <code>priority</code> to control the processing
44
+ # order when multiple directive attributes are used on a template
45
+ # document element.
46
+ #
47
+ #TODO: document the priority hierarchy and convention for level usage
48
+ #
49
+ # Discuss operational context: describe how/when MasterView parser
50
+ # invokes a directive handler in the course of parsing the elements
51
+ # and attributes of a template document node hierarchy.
52
+ # Notion of directive call stack; what state is the world in and
53
+ # information is available to a directive when invoked;
54
+ # what services are available in order to produce some effect
55
+ # on the results of the template parse.
56
+ #
57
+ # Two general flavors on content directives: those which operate on attribute
58
+ # values of the containing element and those which operate on the
59
+ # entire containing element, sometimes by supplying or modifying its
60
+ # content, in other cases by replacing the template element with something
61
+ # else.
62
+ #
63
+ # Third flavor: eval-only directive. Expression which is evaluate for its
64
+ # effect on the directive processing context or the overall state of processing
65
+ # the template document element tree.
66
+ #
67
+ # Directive implementation typically wants to implement either or both of
68
+ # the methods <code>stag(dcs)</code> and <code>etag(dcs)</code> to hook
69
+ # up its processing on the start/end tags of the element on which the
70
+ # attribute is defined.
71
+ #
72
+ # When a directive attribute is used on a template document element,
73
+ # the directive class is instantiated with the attribute_value provided
74
+ # to its constructure. All directives used on an element are sorted
75
+ # into processing order according to their <code>priority</code>
76
+ # (default is <code>Medium</code>. The directive processor is invoked
77
+ # when the element start tag is encountered and when the element end
78
+ # tag is completed, allowing the implementation to control when and
79
+ # how its processing is hooked up to effect the template output.
80
+ #++
81
+ #
82
+ class DirectiveBaseOld
83
+ include PluginLoadTracking
84
+ include DirectiveHelpers
85
+
86
+ # Register a class manually without regard to whether it inherits from DirectiveBase.
87
+ # Classes which derive from DirectiveBase will automatically be registered as they are
88
+ # loaded.
89
+ def self.register_directive(directive_class)
90
+ #ISSUE: do we really need both PluginLoadTracking.register_class
91
+ #and DirectiveBase.register_directive, in addition to MasterView.register_directive???
92
+ #[DJL 04-Jul-2006]
93
+ MasterView.register_directive(directive_class)
94
+ end
95
+
96
+ # Returns the fully qualified attribute name of the directive,
97
+ # consisting of the directive namespace and the directive attribute name.
98
+ #
99
+ # The default MasterView namespace_prefix is used if the directive does not
100
+ # specify a separate namespace.
101
+ #
102
+ #--
103
+ #TODO: clarify the mechanism by which alternate namespaces are defined.
104
+ # Is this done by a code value or configured as part of the directory
105
+ # path specifications, or some combination thereof to allow ovverides
106
+ # in the event of namespace collisions?
107
+ #++
108
+ #
109
+ def self.full_attr_name( namespace_prefix )
110
+ #TODO: fix this so that directives can override to define their own namespace separate from mv:
111
+ namespace_prefix + self.attr_name
112
+ end
113
+
114
+ # Returns the attribute name of the directive.
115
+ #
116
+ # Use full_attr_name to obtain the fully-qualified name
117
+ # of the directive attribute with the qualifying namespace prefix.
118
+ #
119
+ # The default attribute name of a directive is formed
120
+ # from the simple class name (without any module prefix qualifier),
121
+ # with the first character downcased.
122
+ #
123
+ def self.attr_name()
124
+ self.default_attr_name()
125
+ end
126
+
127
+ def self.default_attr_name() #:nodoc:
128
+ self.name.split(':').last.downcase_first_letter
129
+ end
130
+
131
+ # Construct a directive processor for the attribute_value
132
+ # of a directive attribute on a document template element.
133
+ #
134
+ def initialize(attribute_value)
135
+ @attribute_value = attribute_value
136
+ end
137
+
138
+ #if this method exists, it will be called by renderer to save directive_call_stack before each method call
139
+ def save_directive_call_stack(directive_call_stack)
140
+ @directive_call_stack = directive_call_stack
141
+ end
142
+
143
+ # Returns the directive's attribute value string being processed.
144
+ def attr_value
145
+ @attribute_value
146
+ end
147
+
148
+ # Set the directive's attribute value string to be processed.
149
+ def attr_value=(attribute_value)
150
+ @attribute_value = attribute_value
151
+ end
152
+
153
+ #get attribute hash from tag
154
+ def attrs
155
+ @directive_call_stack.context[:tag].attributes
156
+ end
157
+
158
+ #set attribute hash for tag
159
+ def attrs=(attributes)
160
+ @directive_call_stack.context[:tag].attributes = attributes
161
+ end
162
+
163
+ #get attribute hash with lowercased keys and values, and cache it
164
+ def attrs_lckv
165
+ @attrs_lckv ||= lowercase_attribute_keys_and_values(attrs)
166
+ end
167
+
168
+ #get attribute hash with lowercased keys (and original values) and cache it
169
+ def attrs_lck
170
+ @attrs_lck ||= lowercase_attribute_keys(attrs)
171
+ end
172
+
173
+ #returns true if the value for lckey of the attribute hash with lowercased keys and values
174
+ #matches (lowercase) lcmatch string
175
+ def attr_lckv_matches(lckey, lcmatch)
176
+ (attrs_lckv[lckey] && attrs_lckv[lckey] == lcmatch.downcase) ? true : false
177
+ end
178
+
179
+ #DEPRECATED - going away
180
+ #output '<% '+str+' %>'
181
+ def erb(str) #:nodoc:
182
+ #ISSUE: convert clients to erb_eval and drop. Ya oughta have a point of view. [DJL 04-Jul-2006]
183
+ ERB_EVAL_START + str + ERB_EVAL_END
184
+ end
185
+
186
+ # Compose an Erb expression which produces content in the containing document.
187
+ # The expression may also have effects on the processing context of
188
+ # the template document
189
+ #
190
+ # output '<%= '+str+' %>
191
+ #
192
+ def erb_content(str)
193
+ ERB_CONTENT_START + str + ERB_CONTENT_END
194
+ end
195
+
196
+ # Compose an Erb expression which is evaluated for its effect on the processing context
197
+ # but does not produce content in the containing document.
198
+ #
199
+ # output '<% '+str+' %>'
200
+ #
201
+ def erb_eval(str)
202
+ ERB_EVAL_START + str + ERB_EVAL_END
203
+ end
204
+
205
+ #get tag_name
206
+ def tag_name
207
+ @directive_call_stack.context[:tag].tag_name
208
+ end
209
+
210
+ #set tag_name
211
+ def tag_name=(tag_name)
212
+ @directive_call_stack.context[:tag].tag_name = tag_name
213
+ end
214
+
215
+ #inside characters, cdata, or comment you can call this to get the characters passed
216
+ def data
217
+ @directive_call_stack.context[:content_part]
218
+ end
219
+
220
+ #set the data that will be passed to characters, cdata, or comment directives
221
+ def data=(data)
222
+ @directive_call_stack.context[:content_part]=data
223
+ end
224
+
225
+ # rolled up content from all children of the tag, note this will not be complete until hitting the end tag method :etag
226
+ def content
227
+ @directive_call_stack.context[:tag].content
228
+ end
229
+
230
+ #return rolled up content from all children as string, note this will not be complete until hitting the end tag method :etag
231
+ def content_str
232
+ content = @directive_call_stack.context[:tag].content
233
+ content = content.join if content.respond_to? :join
234
+ content
235
+ end
236
+
237
+ # replace the content from all children with a new value
238
+ def content=(content)
239
+ @directive_call_stack.context[:tag].content = content
240
+ end
241
+
242
+ def remove_strings_from_attr_value!
243
+ self.attr_value = remove_prepended_strings(attr_value)
244
+ end
245
+
246
+ #prepend string to attribute value adding a comma if attribute value was not empty
247
+ def prepend_to_attr_value!(str)
248
+ return attr_value if str.nil? || str.strip.empty?
249
+ av = str
250
+ av << ', ' << attr_value unless attr_value.strip.empty?
251
+ self.attr_value = av
252
+ end
253
+
254
+ #append string to attribute value adding a comma if attribute value was not empty
255
+ def append_to_attr_value!(str)
256
+ return attr_value if str.nil? || str.strip.empty?
257
+ av = attr_value
258
+ av << ', ' unless av.strip.empty?
259
+ av << str
260
+ self.attr_value = av
261
+ end
262
+
263
+ #merge merge_hash into hashes stored in attribute_value string
264
+ #hash_index is the zero based index of the hash you want to add to
265
+ def merge_hash_attr_value!(hash_index, merge_hash)
266
+ self.attr_value = merge_into_embedded_hash(attr_value, hash_index, merge_hash)
267
+ end
268
+
269
+ #calls non-evaling parse to split into string arguments
270
+ def parse_attr_value
271
+ parse(attr_value)
272
+ end
273
+
274
+ # check for common html options and return the hash
275
+ def common_html_options(attrs_lck)
276
+ options = {}
277
+ options[:id] = attrs_lck['id'] if attrs_lck['id']
278
+ options[:class] = attrs_lck['class'] if attrs_lck['class']
279
+ options[:style] = attrs_lck['style'] if attrs_lck['style']
280
+ options[:tabindex] = attrs_lck['tabindex'] if attrs_lck['tabindex']
281
+ options[:accesskey] = attrs_lck['accesskey'] if attrs_lck['accesskey']
282
+ options[:disabled] = true if attrs_lck['disabled']
283
+ options[:readonly] = true if attrs_lck['readonly']
284
+ options
285
+ end
286
+
287
+
288
+ # deprecated methods from DirectiveHelpers
289
+ #DEPRECATED
290
+ #remove any strings that were prepended to the hashes, typically these are overridden by other values, so
291
+ #we need to strip them off leaving only the hashes, returns a string with only hashes
292
+ def remove_prepended_strings(full_string)
293
+ return full_string if full_string.nil? || full_string.strip.empty?
294
+ hashes = full_string.scan( /(\{?)\s*(\S+\s*=>.*)/ ).flatten
295
+ hashes.join.strip
296
+ end
297
+
298
+ #DEPRECATED
299
+ #merge hash_to_merge values into the hash contained in the full_string, hash_arg is zero based index of which
300
+ #hash this needes to be merged to if there are multiple ones.
301
+ def merge_into_embedded_hash(full_string, hash_arg, hash_to_merge)
302
+ return full_string if hash_to_merge.empty?
303
+ full_string ||= ""
304
+ sorted_hash_to_merge = hash_to_merge.sort { |a,b| a[0].to_s <=> b[0].to_s } #sort, remember the keys might be symbols so use to_s
305
+ str_to_merge = sorted_hash_to_merge.collect{ |h,v| "#{h.inspect} => #{v.inspect}" }.join(', ')
306
+
307
+ hashes = full_string.scan( /(\{?[^{}]+=>[^{}]+\}?)\s*,?\s*/ ).flatten
308
+ hash_str = hashes[hash_arg] #be careful to use methods to update string in place or else put back in hash
309
+
310
+ if hash_str.nil?
311
+ hashes.each do |v| #make sure each prior hash has brackets, since we are adding a hash
312
+ unless v.index '}'
313
+ v.insert(0, '{')
314
+ v.insert(-1, '}')
315
+ end
316
+ end
317
+ hashes[hash_arg] = hash_str = ""
318
+ end
319
+
320
+ closing_brack = hash_str.index '}'
321
+ if closing_brack
322
+ hash_str.insert(closing_brack, ', '+str_to_merge)
323
+ else
324
+ hash_str << ', ' unless hash_str.empty?
325
+ hash_str << str_to_merge
326
+ end
327
+
328
+ hashes.join(', ')
329
+ end
330
+
331
+ #DEPRECATED
332
+ #return attributes with lowercase keys
333
+ def lowercase_attribute_keys(attributes)
334
+ lcattrs = {}
335
+ attributes.each { |k,v| lcattrs[k.downcase] = v }
336
+ lcattrs
337
+ end
338
+
339
+ #DEPRECATED
340
+ #return attributes with lowercase keys and values
341
+ def lowercase_attribute_keys_and_values(attributes)
342
+ lcattrs = {}
343
+ attributes.each { |k,v| lcattrs[k.downcase] = v.downcase }
344
+ lcattrs
345
+ end
346
+
347
+ #DEPRECATED
348
+ #using hash, symbolize keys, sort keys, serialize to string
349
+ def symbolize_sort_and_serialize_hash_to_str(hash)
350
+ symbolized = {}
351
+ hash.each{ |k,v| symbolized[k.to_sym] = v } #symbolize
352
+ sorted = symbolized.sort{ |a,b| a[0].to_s <=> b[0].to_s } # sort the keys
353
+ sorted_strings = sorted.collect{ |k,v| "#{k.inspect} => '#{v}'"} # create strings
354
+ sorted_strings.join(', ') # finally combine them
355
+ end
356
+
357
+
358
+
359
+
360
+ end
361
+
362
+ end