vpim-rails-reinteractive 0.7

Sign up to get free protection for your applications and to get access to all the features.
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
+