vpim-rails-reinteractive 0.7

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 (60) hide show
  1. data/CHANGES +504 -0
  2. data/COPYING +58 -0
  3. data/README +182 -0
  4. data/lib/atom.rb +728 -0
  5. data/lib/plist.rb +22 -0
  6. data/lib/vpim.rb +13 -0
  7. data/lib/vpim/address.rb +219 -0
  8. data/lib/vpim/attachment.rb +102 -0
  9. data/lib/vpim/date.rb +222 -0
  10. data/lib/vpim/dirinfo.rb +277 -0
  11. data/lib/vpim/duration.rb +119 -0
  12. data/lib/vpim/enumerator.rb +32 -0
  13. data/lib/vpim/field.rb +614 -0
  14. data/lib/vpim/icalendar.rb +386 -0
  15. data/lib/vpim/maker/vcard.rb +16 -0
  16. data/lib/vpim/property/base.rb +193 -0
  17. data/lib/vpim/property/common.rb +315 -0
  18. data/lib/vpim/property/location.rb +38 -0
  19. data/lib/vpim/property/priority.rb +43 -0
  20. data/lib/vpim/property/recurrence.rb +69 -0
  21. data/lib/vpim/property/resources.rb +24 -0
  22. data/lib/vpim/repo.rb +181 -0
  23. data/lib/vpim/rfc2425.rb +372 -0
  24. data/lib/vpim/rrule.rb +598 -0
  25. data/lib/vpim/vcard.rb +1429 -0
  26. data/lib/vpim/version.rb +18 -0
  27. data/lib/vpim/vevent.rb +187 -0
  28. data/lib/vpim/view.rb +90 -0
  29. data/lib/vpim/vjournal.rb +58 -0
  30. data/lib/vpim/vpim.rb +65 -0
  31. data/lib/vpim/vtodo.rb +103 -0
  32. data/samples/README.mutt +93 -0
  33. data/samples/ab-query.rb +57 -0
  34. data/samples/cmd-itip.rb +156 -0
  35. data/samples/ex_cpvcard.rb +55 -0
  36. data/samples/ex_get_vcard_photo.rb +22 -0
  37. data/samples/ex_mkv21vcard.rb +34 -0
  38. data/samples/ex_mkvcard.rb +64 -0
  39. data/samples/ex_mkyourown.rb +29 -0
  40. data/samples/ics-dump.rb +210 -0
  41. data/samples/ics-to-rss.rb +84 -0
  42. data/samples/mutt-aliases-to-vcf.rb +45 -0
  43. data/samples/osx-wrappers.rb +86 -0
  44. data/samples/reminder.rb +203 -0
  45. data/samples/rrule.rb +71 -0
  46. data/samples/tabbed-file-to-vcf.rb +390 -0
  47. data/samples/vcf-dump.rb +86 -0
  48. data/samples/vcf-lines.rb +61 -0
  49. data/samples/vcf-to-ics.rb +22 -0
  50. data/samples/vcf-to-mutt.rb +121 -0
  51. data/test/test_all.rb +17 -0
  52. data/test/test_date.rb +120 -0
  53. data/test/test_dur.rb +41 -0
  54. data/test/test_field.rb +156 -0
  55. data/test/test_ical.rb +415 -0
  56. data/test/test_repo.rb +158 -0
  57. data/test/test_rrule.rb +1030 -0
  58. data/test/test_vcard.rb +973 -0
  59. data/test/test_view.rb +79 -0
  60. metadata +135 -0
@@ -0,0 +1,24 @@
1
+ =begin
2
+ Copyright (C) 2008 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ module Vpim
10
+ class Icalendar
11
+ module Property
12
+
13
+ module Resources
14
+
15
+ def resources
16
+ proptextlistarray 'RESOURCES'
17
+ end
18
+
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+
data/lib/vpim/repo.rb ADDED
@@ -0,0 +1,181 @@
1
+ =begin
2
+ Copyright (C) 2008 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ require 'enumerator'
10
+
11
+ require 'plist'
12
+
13
+ require 'vpim/icalendar'
14
+ require 'vpim/duration'
15
+
16
+ module Vpim
17
+ # A Repo is a representation of a calendar repository.
18
+ #
19
+ # Currently supported repository types are:
20
+ # - Repo::Apple3, an Apple iCal3 repository.
21
+ # - Repo::Directory, a directory hierarchy containing .ics files
22
+ #
23
+ # All repository types support at least the methods of Repo, and all
24
+ # repositories return calendars that support at least the methods of
25
+ # Repo::Calendar.
26
+ class Repo
27
+ include Enumerable
28
+
29
+ # Open a repository at location +where+.
30
+ def initialize(where)
31
+ end
32
+
33
+ # Enumerate the calendars in the repository.
34
+ def each #:yield: calendar
35
+ end
36
+
37
+ # A calendar abstraction. It models a calendar in a calendar repository
38
+ # that may not be an iCalendar.
39
+ #
40
+ # It has methods that behave identically to Icalendar, but it also has
41
+ # methods like name and displayed that are not present in an iCalendar.
42
+ class Calendar
43
+ include Enumerable
44
+
45
+ # The calendar name.
46
+ def name
47
+ end
48
+
49
+ # Whether a calendar should be displayed.
50
+ #
51
+ # TODO - should be #displayed?
52
+ def displayed
53
+ end
54
+
55
+ # Encode into iCalendar format.
56
+ def encode
57
+ end
58
+
59
+ # Enumerate the components in the calendar, both todos and events, or
60
+ # the specified klass. Like Icalendar#each()
61
+ def each(klass=nil, &block) #:yield: component
62
+ end
63
+
64
+ # Enumerate the events in the calendar.
65
+ def events(&block) #:yield: Vevent
66
+ each(Vpim::Icalendar::Vevent, &block)
67
+ end
68
+
69
+ # Enumerate the todos in the calendar.
70
+ def todos(&block) #:yield: Vtodo
71
+ each(Vpim::Icalendar::Vtodo, &block)
72
+ end
73
+
74
+ # The method definitions are just to fool rdoc, not to be used.
75
+ %w{each name displayed encode}.each{|m| remove_method m}
76
+
77
+ def file_each(file, klass, &block) #:nodoc:
78
+ unless iterator?
79
+ return Enumerable::Enumerator.new(self, :each, klass)
80
+ end
81
+
82
+ cals = Vpim::Icalendar.decode(File.open(file))
83
+
84
+ cals.each do |cal|
85
+ cal.each(klass, &block)
86
+ end
87
+ self
88
+ end
89
+ end
90
+ end
91
+
92
+ class Repo
93
+ include Enumerable
94
+
95
+ # An Apple iCal version 3 repository.
96
+ class Apple3 < Repo
97
+ def initialize(where = "~/Library/Calendars")
98
+ @where = where.to_str
99
+ end
100
+
101
+ def each #:nodoc:
102
+ Dir[ File.expand_path(@where + "/**/*.calendar") ].each do |dir|
103
+ yield Calendar.new(dir)
104
+ end
105
+ self
106
+ end
107
+
108
+ class Calendar < Repo::Calendar
109
+ def initialize(dir) # :nodoc:
110
+ @dir = dir
111
+ end
112
+
113
+ def plist(key) #:nodoc:
114
+ Plist::parse_xml( @dir + "/Info.plist")[key]
115
+ end
116
+
117
+ def name #:nodoc:
118
+ plist "Title"
119
+ end
120
+
121
+ def displayed #:nodoc:
122
+ 1 == plist("Checked")
123
+ end
124
+
125
+ def each(klass=nil, &block) #:nodoc:
126
+ unless iterator?
127
+ return Enumerable::Enumerator.new(self, :each, klass)
128
+ end
129
+ Dir[ @dir + "/Events/*.ics" ].map do |ics|
130
+ file_each(ics, klass, &block)
131
+ end
132
+ self
133
+ end
134
+
135
+ def encode #:nodoc:
136
+ Icalendar.create2 do |cal|
137
+ each{|c| cal << c}
138
+ end.encode
139
+ end
140
+ end
141
+
142
+ end
143
+
144
+ class Directory < Repo
145
+ class Calendar < Repo::Calendar
146
+ def initialize(file) #:nodoc:
147
+ @file = file
148
+ end
149
+
150
+ def name #:nodoc:
151
+ File.basename(@file)
152
+ end
153
+
154
+ def displayed #:nodoc:
155
+ true
156
+ end
157
+
158
+ def each(klass, &block) #:nodoc:
159
+ file_each(@file, klass, &block)
160
+ end
161
+
162
+ def encode #:nodoc:
163
+ open(@file, "r"){|f| f.read}
164
+ end
165
+
166
+ end
167
+
168
+ def initialize(where = ".")
169
+ @where = where.to_str
170
+ end
171
+
172
+ def each #:nodoc:
173
+ Dir[ File.expand_path(@where + "/**/*.ics") ].each do |file|
174
+ yield Calendar.new(file)
175
+ end
176
+ self
177
+ end
178
+ end
179
+ end
180
+ end
181
+
@@ -0,0 +1,372 @@
1
+ =begin
2
+ Copyright (C) 2008 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ require 'vpim/vpim'
10
+
11
+ module Vpim
12
+ # Contains regular expression strings for the EBNF of RFC 2425.
13
+ module Bnf #:nodoc:
14
+
15
+ # 1*(ALPHA / DIGIT / "-")
16
+ # Note: I think I can add A-Z here, and get rid of the "i" matches elsewhere.
17
+ # Note: added '_' to allowed because its produced by Notes (X-LOTUS-CHILD_UID:)
18
+ # Note: added '/' to allowed because its produced by KAddressBook (X-messaging/xmpp-All:)
19
+ # Note: added ' ' to allowed because its produced by highrisehq.com (X-GOOGLE TALK:)
20
+ NAME = '[-a-z0-9_/][-a-z0-9_/ ]*'
21
+
22
+ # <"> <Any character except CTLs, DQUOTE> <">
23
+ QSTR = '"([^"]*)"'
24
+
25
+ # *<Any character except CTLs, DQUOTE, ";", ":", ",">
26
+ PTEXT = '([^";:,]+)'
27
+
28
+ # param-value = ptext / quoted-string
29
+ PVALUE = "(?:#{QSTR}|#{PTEXT})"
30
+
31
+ # param = name "=" param-value *("," param-value)
32
+ # Note: v2.1 allows a type or encoding param-value to appear without the type=
33
+ # or the encoding=. This is hideous, but we try and support it, if there
34
+ # is no "=", then $2 will be "", and we will treat it as a v2.1 param.
35
+ PARAM = ";(#{NAME})(=?)((?:#{PVALUE})?(?:,#{PVALUE})*)"
36
+
37
+ # V3.0: contentline = [group "."] name *(";" param) ":" value
38
+ # V2.1: contentline = *( group "." ) name *(";" param) ":" value
39
+ #
40
+ # We accept the V2.1 syntax for backwards compatibility.
41
+ #LINE = "((?:#{NAME}\\.)*)?(#{NAME})([^:]*)\:(.*)"
42
+ LINE = "^((?:#{NAME}\\.)*)?(#{NAME})((?:#{PARAM})*):(.*)$"
43
+
44
+ # date = date-fullyear ["-"] date-month ["-"] date-mday
45
+ # date-fullyear = 4 DIGIT
46
+ # date-month = 2 DIGIT
47
+ # date-mday = 2 DIGIT
48
+ DATE = '(\d\d\d\d)-?(\d\d)-?(\d\d)'
49
+
50
+ # time = time-hour [":"] time-minute [":"] time-second [time-secfrac] [time-zone]
51
+ # time-hour = 2 DIGIT
52
+ # time-minute = 2 DIGIT
53
+ # time-second = 2 DIGIT
54
+ # time-secfrac = "," 1*DIGIT
55
+ # time-zone = "Z" / time-numzone
56
+ # time-numzone = sign time-hour [":"] time-minute
57
+ TIME = '(\d\d):?(\d\d):?(\d\d)(\.\d+)?(Z|[-+]\d\d:?\d\d)?'
58
+
59
+ # integer = (["+"] / "-") 1*DIGIT
60
+ INTEGER = '[-+]?\d+'
61
+
62
+ # QSAFE-CHAR = WSP / %x21 / %x23-7E / NON-US-ASCII
63
+ # ; Any character except CTLs and DQUOTE
64
+ QSAFECHAR = '[ \t\x21\x23-\x7e\x80-\xff]'
65
+
66
+ # SAFE-CHAR = WSP / %x21 / %x23-2B / %x2D-39 / %x3C-7E / NON-US-ASCII
67
+ # ; Any character except CTLs, DQUOTE, ";", ":", ","
68
+ SAFECHAR = '[ \t\x21\x23-\x2b\x2d-\x39\x3c-\x7e\x80-\xff]'
69
+ end
70
+ end
71
+
72
+ module Vpim
73
+ # Split on \r\n or \n to get the lines, unfold continued lines (they
74
+ # start with ' ' or \t), and return the array of unfolded lines.
75
+ #
76
+ # This also supports the (invalid) encoding convention of allowing empty
77
+ # lines to be inserted for readability - it does this by dropping zero-length
78
+ # lines.
79
+ def Vpim.unfold(card) #:nodoc:
80
+ unfolded = []
81
+
82
+ card.each do |line|
83
+ line.chomp!
84
+ # If it's a continuation line, add it to the last.
85
+ # If it's an empty line, drop it from the input.
86
+ if( line =~ /^[ \t]/ )
87
+ unfolded[-1] << line[1, line.size-1]
88
+ elsif( line =~ /^$/ )
89
+ else
90
+ unfolded << line
91
+ end
92
+ end
93
+
94
+ unfolded
95
+ end
96
+
97
+ # Convert a +sep+-seperated list of values into an array of values.
98
+ def Vpim.decode_list(value, sep = ',') # :nodoc:
99
+ list = []
100
+
101
+ value.each(sep) do |item|
102
+ item.chomp!(sep)
103
+ list << yield(item)
104
+ end
105
+ list
106
+ end
107
+
108
+ # Convert a RFC 2425 date into an array of [year, month, day].
109
+ def Vpim.decode_date(v) # :nodoc:
110
+ unless v =~ %r{^\s*#{Bnf::DATE}\s*$}
111
+ raise Vpim::InvalidEncodingError, "date not valid (#{v})"
112
+ end
113
+ [$1.to_i, $2.to_i, $3.to_i]
114
+ end
115
+
116
+ # Convert a RFC 2425 date into a Date object.
117
+ def self.decode_date_to_date(v)
118
+ Date.new(*decode_date(v))
119
+ end
120
+
121
+ # Note in the following the RFC2425 allows yyyy-mm-ddThh:mm:ss, but RFC2445
122
+ # does not. I choose to encode to the subset that is valid for both.
123
+
124
+ # Encode a Date object as "yyyymmdd".
125
+ def Vpim.encode_date(d) # :nodoc:
126
+ "%0.4d%0.2d%0.2d" % [ d.year, d.mon, d.day ]
127
+ end
128
+
129
+ # Encode a Date object as "yyyymmdd".
130
+ def Vpim.encode_time(d) # :nodoc:
131
+ "%0.4d%0.2d%0.2d" % [ d.year, d.mon, d.day ]
132
+ end
133
+
134
+ # Encode a Time or DateTime object as "yyyymmddThhmmss"
135
+ def Vpim.encode_date_time(d) # :nodoc:
136
+ case d
137
+ when ActiveSupport::TimeWithZone
138
+ d.utc.strftime("%Y%m%dT%H%M%SZ")
139
+ else
140
+ "%0.4d%0.2d%0.2dT%0.2d%0.2d%0.2d" % [ d.year, d.mon, d.day, d.hour, d.min, d.sec ]
141
+ end
142
+ end
143
+
144
+ # Convert a RFC 2425 time into an array of [hour,min,sec,secfrac,timezone]
145
+ def Vpim.decode_time(v) # :nodoc:
146
+ unless match = %r{^\s*#{Bnf::TIME}\s*$}.match(v)
147
+ raise Vpim::InvalidEncodingError, "time '#{v}' not valid"
148
+ end
149
+ hour, min, sec, secfrac, tz = match.to_a[1..5]
150
+
151
+ [hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz]
152
+ end
153
+
154
+ def self.array_datetime_to_time(dtarray) #:nodoc:
155
+ # We get [ year, month, day, hour, min, sec, usec, tz ]
156
+ begin
157
+ tz = (dtarray.pop == "Z") ? :gm : :local
158
+ Time.send(tz, *dtarray)
159
+ rescue ArgumentError => e
160
+ raise Vpim::InvalidEncodingError, "#{tz} #{e} (#{dtarray.join(', ')})"
161
+ end
162
+ end
163
+
164
+ # Convert a RFC 2425 time into an array of Time objects.
165
+ def Vpim.decode_time_to_time(v) # :nodoc:
166
+ array_datetime_to_time(decode_date_time(v))
167
+ end
168
+
169
+ # Convert a RFC 2425 date-time into an array of [year,mon,day,hour,min,sec,secfrac,timezone]
170
+ def Vpim.decode_date_time(v) # :nodoc:
171
+ unless match = %r{^\s*#{Bnf::DATE}T#{Bnf::TIME}\s*$}.match(v)
172
+ raise Vpim::InvalidEncodingError, "date-time '#{v}' not valid"
173
+ end
174
+ year, month, day, hour, min, sec, secfrac, tz = match.to_a[1..8]
175
+
176
+ [
177
+ # date
178
+ year.to_i, month.to_i, day.to_i,
179
+ # time
180
+ hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz
181
+ ]
182
+ end
183
+
184
+ def Vpim.decode_date_time_to_datetime(v) #:nodoc:
185
+ year, month, day, hour, min, sec, secfrac, tz = Vpim.decode_date_time(v)
186
+ # TODO - DateTime understands timezones, so we could decode tz and use it.
187
+ DateTime.civil(year, month, day, hour, min, sec, 0)
188
+ end
189
+
190
+ # Vpim.decode_boolean
191
+ #
192
+ # float
193
+ #
194
+ # float_list
195
+ =begin
196
+ =end
197
+
198
+ # Convert an RFC2425 INTEGER value into an Integer
199
+ def Vpim.decode_integer(v) # :nodoc:
200
+ unless match = %r{\s*#{Bnf::INTEGER}\s*}.match(v)
201
+ raise Vpim::InvalidEncodingError, "integer not valid (#{v})"
202
+ end
203
+ v.to_i
204
+ end
205
+
206
+ #
207
+ # integer_list
208
+
209
+ # Convert a RFC2425 date-list into an array of dates.
210
+ def Vpim.decode_date_list(v) # :nodoc:
211
+ Vpim.decode_list(v) do |date|
212
+ date.strip!
213
+ if date.length > 0
214
+ Vpim.decode_date(date)
215
+ end
216
+ end.compact
217
+ end
218
+
219
+ # Convert a RFC 2425 time-list into an array of times.
220
+ def Vpim.decode_time_list(v) # :nodoc:
221
+ Vpim.decode_list(v) do |time|
222
+ time.strip!
223
+ if time.length > 0
224
+ Vpim.decode_time(time)
225
+ end
226
+ end.compact
227
+ end
228
+
229
+ # Convert a RFC 2425 date-time-list into an array of date-times.
230
+ def Vpim.decode_date_time_list(v) # :nodoc:
231
+ Vpim.decode_list(v) do |datetime|
232
+ datetime.strip!
233
+ if datetime.length > 0
234
+ Vpim.decode_date_time(datetime)
235
+ end
236
+ end.compact
237
+ end
238
+
239
+ # Convert RFC 2425 text into a String.
240
+ # \\ -> \
241
+ # \n -> NL
242
+ # \N -> NL
243
+ # \, -> ,
244
+ # \; -> ;
245
+ #
246
+ # I've seen double-quote escaped by iCal.app. Hmm. Ok, if you aren't supposed
247
+ # to escape anything but the above, everything else is ambiguous, so I'll
248
+ # just support it.
249
+ def Vpim.decode_text(v) # :nodoc:
250
+ # FIXME - I think this should trim leading and trailing space
251
+ v.gsub(/\\(.)/) do
252
+ case $1
253
+ when 'n', 'N'
254
+ "\n"
255
+ else
256
+ $1
257
+ end
258
+ end
259
+ end
260
+
261
+ def Vpim.encode_text(v) #:nodoc:
262
+ v.to_str.gsub(/([\\,;\n])/) { $1 == "\n" ? "\\n" : "\\"+$1 }
263
+ end
264
+
265
+ # v is an Array of String, or just a single String
266
+ def Vpim.encode_text_list(v, sep = ",") #:nodoc:
267
+ begin
268
+ v.to_ary.map{ |t| Vpim.encode_text(t) }.join(sep)
269
+ rescue
270
+ Vpim.encode_text(v)
271
+ end
272
+ end
273
+
274
+ # Convert a +sep+-seperated list of TEXT values into an array of values.
275
+ def Vpim.decode_text_list(value, sep = ',') # :nodoc:
276
+ # Need to do in two stages, as best I can find.
277
+ list = value.scan(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)#{sep}/).map do |v|
278
+ Vpim.decode_text(v.first)
279
+ end
280
+ if value.match(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)$/)
281
+ list << $1
282
+ end
283
+ list
284
+ end
285
+
286
+ # param-value = paramtext / quoted-string
287
+ # paramtext = *SAFE-CHAR
288
+ # quoted-string = DQUOTE *QSAFE-CHAR DQUOTE
289
+ def Vpim.encode_paramtext(value)
290
+ case value
291
+ when %r{\A#{Bnf::SAFECHAR}*\z}
292
+ value
293
+ else
294
+ raise Vpim::Unencodable, "paramtext #{value.inspect}"
295
+ end
296
+ end
297
+
298
+ def Vpim.encode_paramvalue(value)
299
+ case value
300
+ when %r{\A#{Bnf::SAFECHAR}*\z}
301
+ value
302
+ when %r{\A#{Bnf::QSAFECHAR}*\z}
303
+ '"' + value + '"'
304
+ else
305
+ raise Vpim::Unencodable, "param-value #{value.inspect}"
306
+ end
307
+ end
308
+
309
+
310
+ # Unfold the lines in +card+, then return an array of one Field object per
311
+ # line.
312
+ def Vpim.decode(card) #:nodoc:
313
+ content = Vpim.unfold(card).collect { |line| DirectoryInfo::Field.decode(line) }
314
+ end
315
+
316
+
317
+ # Expand an array of fields into its syntactic entities. Each entity is a sequence
318
+ # of fields where the sequences is delimited by a BEGIN/END field. Since
319
+ # BEGIN/END delimited entities can be nested, we build a tree. Each entry in
320
+ # the array is either a Field or an array of entries (where each entry is
321
+ # either a Field, or an array of entries...).
322
+ def Vpim.expand(src) #:nodoc:
323
+ # output array to expand the src to
324
+ dst = []
325
+ # stack used to track our nesting level, as we see begin/end we start a
326
+ # new/finish the current entity, and push/pop that entity from the stack
327
+ current = [ dst ]
328
+
329
+ for f in src
330
+ if f.name? 'BEGIN'
331
+ e = [ f ]
332
+
333
+ current.last.push(e)
334
+ current.push(e)
335
+
336
+ elsif f.name? 'END'
337
+ current.last.push(f)
338
+
339
+ unless current.last.first.value? current.last.last.value
340
+ raise "BEGIN/END mismatch (#{current.last.first.value} != #{current.last.last.value})"
341
+ end
342
+
343
+ current.pop
344
+
345
+ else
346
+ current.last.push(f)
347
+ end
348
+ end
349
+
350
+ dst
351
+ end
352
+
353
+ # Split an array into an array of all the fields at the outer level, and
354
+ # an array of all the inner arrays of fields. Return the array [outer,
355
+ # inner].
356
+ def Vpim.outer_inner(fields) #:nodoc:
357
+ # TODO - use Enumerable#partition
358
+ # seperate into the outer-level fields, and the arrays of component
359
+ # fields
360
+ outer = []
361
+ inner = []
362
+ fields.each do |line|
363
+ case line
364
+ when Array; inner << line
365
+ else; outer << line
366
+ end
367
+ end
368
+ return outer, inner
369
+ end
370
+
371
+ end
372
+