vpim-rails 0.661

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) 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 +382 -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 +367 -0
  24. data/lib/vpim/rrule.rb +599 -0
  25. data/lib/vpim/time.rb +40 -0
  26. data/lib/vpim/vcard.rb +1429 -0
  27. data/lib/vpim/version.rb +18 -0
  28. data/lib/vpim/vevent.rb +187 -0
  29. data/lib/vpim/view.rb +90 -0
  30. data/lib/vpim/vjournal.rb +58 -0
  31. data/lib/vpim/vpim.rb +65 -0
  32. data/lib/vpim/vtodo.rb +103 -0
  33. data/samples/README.mutt +93 -0
  34. data/samples/ab-query.rb +57 -0
  35. data/samples/cmd-itip.rb +156 -0
  36. data/samples/ex_cpvcard.rb +55 -0
  37. data/samples/ex_get_vcard_photo.rb +22 -0
  38. data/samples/ex_mkv21vcard.rb +34 -0
  39. data/samples/ex_mkvcard.rb +64 -0
  40. data/samples/ex_mkyourown.rb +29 -0
  41. data/samples/ics-dump.rb +210 -0
  42. data/samples/ics-to-rss.rb +84 -0
  43. data/samples/mutt-aliases-to-vcf.rb +45 -0
  44. data/samples/osx-wrappers.rb +86 -0
  45. data/samples/reminder.rb +203 -0
  46. data/samples/rrule.rb +71 -0
  47. data/samples/tabbed-file-to-vcf.rb +390 -0
  48. data/samples/vcf-dump.rb +86 -0
  49. data/samples/vcf-lines.rb +61 -0
  50. data/samples/vcf-to-ics.rb +22 -0
  51. data/samples/vcf-to-mutt.rb +121 -0
  52. data/test/test_all.rb +17 -0
  53. data/test/test_date.rb +120 -0
  54. data/test/test_dur.rb +41 -0
  55. data/test/test_field.rb +156 -0
  56. data/test/test_ical.rb +415 -0
  57. data/test/test_repo.rb +158 -0
  58. data/test/test_rrule.rb +1030 -0
  59. data/test/test_vcard.rb +973 -0
  60. data/test/test_view.rb +79 -0
  61. metadata +126 -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,367 @@
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
+ "%0.4d%0.2d%0.2dT%0.2d%0.2d%0.2d" % [ d.year, d.mon, d.day, d.hour, d.min, d.sec ]
137
+ end
138
+
139
+ # Convert a RFC 2425 time into an array of [hour,min,sec,secfrac,timezone]
140
+ def Vpim.decode_time(v) # :nodoc:
141
+ unless match = %r{^\s*#{Bnf::TIME}\s*$}.match(v)
142
+ raise Vpim::InvalidEncodingError, "time '#{v}' not valid"
143
+ end
144
+ hour, min, sec, secfrac, tz = match.to_a[1..5]
145
+
146
+ [hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz]
147
+ end
148
+
149
+ def self.array_datetime_to_time(dtarray) #:nodoc:
150
+ # We get [ year, month, day, hour, min, sec, usec, tz ]
151
+ begin
152
+ tz = (dtarray.pop == "Z") ? :gm : :local
153
+ Time.send(tz, *dtarray)
154
+ rescue ArgumentError => e
155
+ raise Vpim::InvalidEncodingError, "#{tz} #{e} (#{dtarray.join(', ')})"
156
+ end
157
+ end
158
+
159
+ # Convert a RFC 2425 time into an array of Time objects.
160
+ def Vpim.decode_time_to_time(v) # :nodoc:
161
+ array_datetime_to_time(decode_date_time(v))
162
+ end
163
+
164
+ # Convert a RFC 2425 date-time into an array of [year,mon,day,hour,min,sec,secfrac,timezone]
165
+ def Vpim.decode_date_time(v) # :nodoc:
166
+ unless match = %r{^\s*#{Bnf::DATE}T#{Bnf::TIME}\s*$}.match(v)
167
+ raise Vpim::InvalidEncodingError, "date-time '#{v}' not valid"
168
+ end
169
+ year, month, day, hour, min, sec, secfrac, tz = match.to_a[1..8]
170
+
171
+ [
172
+ # date
173
+ year.to_i, month.to_i, day.to_i,
174
+ # time
175
+ hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz
176
+ ]
177
+ end
178
+
179
+ def Vpim.decode_date_time_to_datetime(v) #:nodoc:
180
+ year, month, day, hour, min, sec, secfrac, tz = Vpim.decode_date_time(v)
181
+ # TODO - DateTime understands timezones, so we could decode tz and use it.
182
+ DateTime.civil(year, month, day, hour, min, sec, 0)
183
+ end
184
+
185
+ # Vpim.decode_boolean
186
+ #
187
+ # float
188
+ #
189
+ # float_list
190
+ =begin
191
+ =end
192
+
193
+ # Convert an RFC2425 INTEGER value into an Integer
194
+ def Vpim.decode_integer(v) # :nodoc:
195
+ unless match = %r{\s*#{Bnf::INTEGER}\s*}.match(v)
196
+ raise Vpim::InvalidEncodingError, "integer not valid (#{v})"
197
+ end
198
+ v.to_i
199
+ end
200
+
201
+ #
202
+ # integer_list
203
+
204
+ # Convert a RFC2425 date-list into an array of dates.
205
+ def Vpim.decode_date_list(v) # :nodoc:
206
+ Vpim.decode_list(v) do |date|
207
+ date.strip!
208
+ if date.length > 0
209
+ Vpim.decode_date(date)
210
+ end
211
+ end.compact
212
+ end
213
+
214
+ # Convert a RFC 2425 time-list into an array of times.
215
+ def Vpim.decode_time_list(v) # :nodoc:
216
+ Vpim.decode_list(v) do |time|
217
+ time.strip!
218
+ if time.length > 0
219
+ Vpim.decode_time(time)
220
+ end
221
+ end.compact
222
+ end
223
+
224
+ # Convert a RFC 2425 date-time-list into an array of date-times.
225
+ def Vpim.decode_date_time_list(v) # :nodoc:
226
+ Vpim.decode_list(v) do |datetime|
227
+ datetime.strip!
228
+ if datetime.length > 0
229
+ Vpim.decode_date_time(datetime)
230
+ end
231
+ end.compact
232
+ end
233
+
234
+ # Convert RFC 2425 text into a String.
235
+ # \\ -> \
236
+ # \n -> NL
237
+ # \N -> NL
238
+ # \, -> ,
239
+ # \; -> ;
240
+ #
241
+ # I've seen double-quote escaped by iCal.app. Hmm. Ok, if you aren't supposed
242
+ # to escape anything but the above, everything else is ambiguous, so I'll
243
+ # just support it.
244
+ def Vpim.decode_text(v) # :nodoc:
245
+ # FIXME - I think this should trim leading and trailing space
246
+ v.gsub(/\\(.)/) do
247
+ case $1
248
+ when 'n', 'N'
249
+ "\n"
250
+ else
251
+ $1
252
+ end
253
+ end
254
+ end
255
+
256
+ def Vpim.encode_text(v) #:nodoc:
257
+ v.to_str.gsub(/([\\,;\n])/) { $1 == "\n" ? "\\n" : "\\"+$1 }
258
+ end
259
+
260
+ # v is an Array of String, or just a single String
261
+ def Vpim.encode_text_list(v, sep = ",") #:nodoc:
262
+ begin
263
+ v.to_ary.map{ |t| Vpim.encode_text(t) }.join(sep)
264
+ rescue
265
+ Vpim.encode_text(v)
266
+ end
267
+ end
268
+
269
+ # Convert a +sep+-seperated list of TEXT values into an array of values.
270
+ def Vpim.decode_text_list(value, sep = ',') # :nodoc:
271
+ # Need to do in two stages, as best I can find.
272
+ list = value.scan(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)#{sep}/).map do |v|
273
+ Vpim.decode_text(v.first)
274
+ end
275
+ if value.match(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)$/)
276
+ list << $1
277
+ end
278
+ list
279
+ end
280
+
281
+ # param-value = paramtext / quoted-string
282
+ # paramtext = *SAFE-CHAR
283
+ # quoted-string = DQUOTE *QSAFE-CHAR DQUOTE
284
+ def Vpim.encode_paramtext(value)
285
+ case value
286
+ when %r{\A#{Bnf::SAFECHAR}*\z}
287
+ value
288
+ else
289
+ raise Vpim::Unencodable, "paramtext #{value.inspect}"
290
+ end
291
+ end
292
+
293
+ def Vpim.encode_paramvalue(value)
294
+ case value
295
+ when %r{\A#{Bnf::SAFECHAR}*\z}
296
+ value
297
+ when %r{\A#{Bnf::QSAFECHAR}*\z}
298
+ '"' + value + '"'
299
+ else
300
+ raise Vpim::Unencodable, "param-value #{value.inspect}"
301
+ end
302
+ end
303
+
304
+
305
+ # Unfold the lines in +card+, then return an array of one Field object per
306
+ # line.
307
+ def Vpim.decode(card) #:nodoc:
308
+ content = Vpim.unfold(card).collect { |line| DirectoryInfo::Field.decode(line) }
309
+ end
310
+
311
+
312
+ # Expand an array of fields into its syntactic entities. Each entity is a sequence
313
+ # of fields where the sequences is delimited by a BEGIN/END field. Since
314
+ # BEGIN/END delimited entities can be nested, we build a tree. Each entry in
315
+ # the array is either a Field or an array of entries (where each entry is
316
+ # either a Field, or an array of entries...).
317
+ def Vpim.expand(src) #:nodoc:
318
+ # output array to expand the src to
319
+ dst = []
320
+ # stack used to track our nesting level, as we see begin/end we start a
321
+ # new/finish the current entity, and push/pop that entity from the stack
322
+ current = [ dst ]
323
+
324
+ for f in src
325
+ if f.name? 'BEGIN'
326
+ e = [ f ]
327
+
328
+ current.last.push(e)
329
+ current.push(e)
330
+
331
+ elsif f.name? 'END'
332
+ current.last.push(f)
333
+
334
+ unless current.last.first.value? current.last.last.value
335
+ raise "BEGIN/END mismatch (#{current.last.first.value} != #{current.last.last.value})"
336
+ end
337
+
338
+ current.pop
339
+
340
+ else
341
+ current.last.push(f)
342
+ end
343
+ end
344
+
345
+ dst
346
+ end
347
+
348
+ # Split an array into an array of all the fields at the outer level, and
349
+ # an array of all the inner arrays of fields. Return the array [outer,
350
+ # inner].
351
+ def Vpim.outer_inner(fields) #:nodoc:
352
+ # TODO - use Enumerable#partition
353
+ # seperate into the outer-level fields, and the arrays of component
354
+ # fields
355
+ outer = []
356
+ inner = []
357
+ fields.each do |line|
358
+ case line
359
+ when Array; inner << line
360
+ else; outer << line
361
+ end
362
+ end
363
+ return outer, inner
364
+ end
365
+
366
+ end
367
+