vcard 0.1.1 → 0.2.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.
- data/.gitignore +15 -5
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +17 -0
- data/Rakefile +4 -52
- data/{LICENSE → VPIM-LICENSE.txt} +2 -2
- data/lib/vcard.rb +304 -28
- data/lib/vcard/attachment.rb +10 -12
- data/lib/vcard/bnf.rb +66 -0
- data/lib/vcard/dirinfo.rb +24 -26
- data/lib/vcard/enumerator.rb +5 -7
- data/lib/vcard/errors.rb +23 -0
- data/lib/vcard/field.rb +56 -58
- data/lib/vcard/vcard.rb +210 -240
- data/lib/vcard/version.rb +3 -0
- data/test/field_test.rb +55 -55
- data/test/fixtures/bday_decode.vcard +3 -0
- data/test/fixtures/bday_decode_2.vcard +6 -0
- data/test/fixtures/empty_tel.vcard +3 -0
- data/test/fixtures/ex1.vcard +7 -0
- data/test/fixtures/ex2.vcard +9 -0
- data/test/fixtures/ex3.vcard +30 -0
- data/test/fixtures/ex_21.vcard +16 -0
- data/test/fixtures/ex_21_case0.vcard +15 -0
- data/test/fixtures/ex_apple1.vcard +13 -0
- data/test/fixtures/ex_attach.vcard +16 -0
- data/test/fixtures/ex_bdays.vcard +8 -0
- data/test/fixtures/ex_encode_1.vcard +10 -0
- data/test/fixtures/ex_ical_1.vcal +47 -0
- data/test/fixtures/gmail.vcard +27 -0
- data/test/fixtures/highrise.vcard +41 -0
- data/test/fixtures/multiple_occurences_of_type.vcard +17 -0
- data/test/fixtures/nickname0.vcard +2 -0
- data/test/fixtures/nickname1.vcard +3 -0
- data/test/fixtures/nickname2.vcard +3 -0
- data/test/fixtures/nickname3.vcard +3 -0
- data/test/fixtures/nickname4.vcard +4 -0
- data/test/fixtures/nickname5.vcard +5 -0
- data/test/fixtures/slash_in_field_name.vcard +3 -0
- data/test/fixtures/tst1.vcard +9 -0
- data/test/fixtures/url_decode.vcard +4 -0
- data/test/test_helper.rb +34 -6
- data/test/vcard_test.rb +87 -577
- data/vcard.gemspec +19 -0
- metadata +88 -43
- data/.document +0 -5
- data/README.rdoc +0 -7
- data/VERSION +0 -1
- data/lib/vcard/rfc2425.rb +0 -367
data/.gitignore
CHANGED
@@ -1,7 +1,17 @@
|
|
1
|
-
*.
|
2
|
-
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
3
9
|
coverage
|
4
|
-
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
5
12
|
pkg
|
6
|
-
|
7
|
-
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Kuba Kuźma
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
data/Rakefile
CHANGED
@@ -1,58 +1,10 @@
|
|
1
|
-
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rake/testtask"
|
2
3
|
|
3
|
-
require 'rubygems'
|
4
|
-
require 'rake'
|
5
|
-
|
6
|
-
begin
|
7
|
-
require 'jeweler'
|
8
|
-
Jeweler::Tasks.new do |gem|
|
9
|
-
gem.name = "vcard"
|
10
|
-
gem.summary = %Q{Vcard support extracted from Vpim (Ruby 1.9.1 compatible)}
|
11
|
-
gem.email = "qoobaa@gmail.com"
|
12
|
-
gem.homepage = "http://github.com/qoobaa/vcard"
|
13
|
-
gem.authors = ["Jakub Kuźma"]
|
14
|
-
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
|
-
end
|
16
|
-
|
17
|
-
rescue LoadError
|
18
|
-
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
19
|
-
end
|
20
|
-
|
21
|
-
require 'rake/testtask'
|
22
4
|
Rake::TestTask.new(:test) do |test|
|
23
|
-
test.libs <<
|
24
|
-
test.pattern =
|
5
|
+
test.libs << "lib" << "test"
|
6
|
+
test.pattern = "test/**/*_test.rb"
|
25
7
|
test.verbose = true
|
26
8
|
end
|
27
9
|
|
28
|
-
begin
|
29
|
-
require 'rcov/rcovtask'
|
30
|
-
Rcov::RcovTask.new do |test|
|
31
|
-
test.libs << 'test'
|
32
|
-
test.pattern = 'test/**/*_test.rb'
|
33
|
-
test.verbose = true
|
34
|
-
end
|
35
|
-
rescue LoadError
|
36
|
-
task :rcov do
|
37
|
-
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
|
42
10
|
task :default => :test
|
43
|
-
|
44
|
-
require 'rake/rdoctask'
|
45
|
-
Rake::RDocTask.new do |rdoc|
|
46
|
-
if File.exist?('VERSION.yml')
|
47
|
-
config = YAML.load(File.read('VERSION.yml'))
|
48
|
-
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
49
|
-
else
|
50
|
-
version = ""
|
51
|
-
end
|
52
|
-
|
53
|
-
rdoc.rdoc_dir = 'rdoc'
|
54
|
-
rdoc.title = "vcard #{version}"
|
55
|
-
rdoc.rdoc_files.include('README*')
|
56
|
-
rdoc.rdoc_files.include('lib/**/*.rb')
|
57
|
-
end
|
58
|
-
|
@@ -45,9 +45,9 @@ the file GPL), or the conditions below:
|
|
45
45
|
For the list of those files and their copying conditions, see the
|
46
46
|
file LEGAL.
|
47
47
|
|
48
|
-
5. The scripts and library files supplied as input to or produced as
|
48
|
+
5. The scripts and library files supplied as input to or produced as
|
49
49
|
output from the software do not automatically fall under the
|
50
|
-
copyright of the software, but belong to whomever generated them,
|
50
|
+
copyright of the software, but belong to whomever generated them,
|
51
51
|
and may be sold commercially, and may be aggregated with this
|
52
52
|
software.
|
53
53
|
|
data/lib/vcard.rb
CHANGED
@@ -1,34 +1,310 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
#:title:vPim - vCard and iCalendar support for Ruby
|
11
|
-
module Vpim
|
12
|
-
# Exception used to indicate that data being decoded is invalid, the message
|
13
|
-
# should describe what is invalid.
|
14
|
-
class InvalidEncodingError < StandardError; end
|
15
|
-
|
16
|
-
# Exception used to indicate that data being decoded is unsupported, the message
|
17
|
-
# should describe what is unsupported.
|
18
|
-
#
|
19
|
-
# If its unsupported, its likely because I didn't anticipate it being useful
|
20
|
-
# to support this, and it likely it could be supported on request.
|
21
|
-
class UnsupportedError < StandardError; end
|
22
|
-
|
23
|
-
# Exception used to indicate that encoding failed, probably because the
|
24
|
-
# object would not result in validly encoded data. The message should
|
25
|
-
# describe what is unsupported.
|
26
|
-
class Unencodeable < StandardError; end
|
27
|
-
end
|
1
|
+
# Copyright (C) 2008 Sam Roberts
|
2
|
+
|
3
|
+
# This library is free software; you can redistribute it and/or modify
|
4
|
+
# it under the same terms as the ruby language itself, see the file
|
5
|
+
# LICENSE-VPIM.txt for details.
|
6
|
+
|
7
|
+
require "date"
|
8
|
+
require "open-uri"
|
9
|
+
require "stringio"
|
28
10
|
|
29
11
|
require "vcard/attachment"
|
12
|
+
require "vcard/bnf"
|
30
13
|
require "vcard/dirinfo"
|
31
14
|
require "vcard/enumerator"
|
15
|
+
require "vcard/errors"
|
32
16
|
require "vcard/field"
|
33
|
-
require "vcard/rfc2425"
|
34
17
|
require "vcard/vcard"
|
18
|
+
|
19
|
+
module Vcard
|
20
|
+
# Split on \r\n or \n to get the lines, unfold continued lines (they
|
21
|
+
# start with " " or \t), and return the array of unfolded lines.
|
22
|
+
#
|
23
|
+
# This also supports the (invalid) encoding convention of allowing empty
|
24
|
+
# lines to be inserted for readability - it does this by dropping zero-length
|
25
|
+
# lines.
|
26
|
+
def self.unfold(card) #:nodoc:
|
27
|
+
unfolded = []
|
28
|
+
|
29
|
+
card.lines do |line|
|
30
|
+
line.chomp!
|
31
|
+
# If it's a continuation line, add it to the last.
|
32
|
+
# If it's an empty line, drop it from the input.
|
33
|
+
if( line =~ /^[ \t]/ )
|
34
|
+
unfolded[-1] << line[1, line.size-1]
|
35
|
+
elsif( line =~ /^$/ )
|
36
|
+
else
|
37
|
+
unfolded << line
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
unfolded
|
42
|
+
end
|
43
|
+
|
44
|
+
# Convert a +sep+-seperated list of values into an array of values.
|
45
|
+
def self.decode_list(value, sep = ",") # :nodoc:
|
46
|
+
list = []
|
47
|
+
|
48
|
+
value.split(sep).each do |item|
|
49
|
+
item.chomp!(sep)
|
50
|
+
list << yield(item)
|
51
|
+
end
|
52
|
+
list
|
53
|
+
end
|
54
|
+
|
55
|
+
# Convert a RFC 2425 date into an array of [year, month, day].
|
56
|
+
def self.decode_date(v) # :nodoc:
|
57
|
+
unless v =~ %r{^\s*#{Bnf::DATE}\s*$}
|
58
|
+
raise Vcard::InvalidEncodingError, "date not valid (#{v})"
|
59
|
+
end
|
60
|
+
[$1.to_i, $2.to_i, $3.to_i]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Convert a RFC 2425 date into a Date object.
|
64
|
+
def self.decode_date_to_date(v)
|
65
|
+
Date.new(*decode_date(v))
|
66
|
+
end
|
67
|
+
|
68
|
+
# Note in the following the RFC2425 allows yyyy-mm-ddThh:mm:ss, but RFC2445
|
69
|
+
# does not. I choose to encode to the subset that is valid for both.
|
70
|
+
|
71
|
+
# Encode a Date object as "yyyymmdd".
|
72
|
+
def self.encode_date(d) # :nodoc:
|
73
|
+
"%0.4d%0.2d%0.2d" % [ d.year, d.mon, d.day ]
|
74
|
+
end
|
75
|
+
|
76
|
+
# Encode a Date object as "yyyymmdd".
|
77
|
+
def self.encode_time(d) # :nodoc:
|
78
|
+
"%0.4d%0.2d%0.2d" % [ d.year, d.mon, d.day ]
|
79
|
+
end
|
80
|
+
|
81
|
+
# Encode a Time or DateTime object as "yyyymmddThhmmss"
|
82
|
+
def self.encode_date_time(d) # :nodoc:
|
83
|
+
"%0.4d%0.2d%0.2dT%0.2d%0.2d%0.2d" % [ d.year, d.mon, d.day, d.hour, d.min, d.sec ]
|
84
|
+
end
|
85
|
+
|
86
|
+
# Convert a RFC 2425 time into an array of [hour,min,sec,secfrac,timezone]
|
87
|
+
def self.decode_time(v) # :nodoc:
|
88
|
+
unless match = %r{^\s*#{Bnf::TIME}\s*$}.match(v)
|
89
|
+
raise Vcard::InvalidEncodingError, "time '#{v}' not valid"
|
90
|
+
end
|
91
|
+
hour, min, sec, secfrac, tz = match.to_a[1..5]
|
92
|
+
|
93
|
+
[hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz]
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.array_datetime_to_time(dtarray) #:nodoc:
|
97
|
+
# We get [ year, month, day, hour, min, sec, usec, tz ]
|
98
|
+
begin
|
99
|
+
tz = (dtarray.pop == "Z") ? :gm : :local
|
100
|
+
Time.send(tz, *dtarray)
|
101
|
+
rescue ArgumentError => e
|
102
|
+
raise Vcard::InvalidEncodingError, "#{tz} #{e} (#{dtarray.join(', ')})"
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Convert a RFC 2425 time into an array of Time objects.
|
107
|
+
def self.decode_time_to_time(v) # :nodoc:
|
108
|
+
array_datetime_to_time(decode_date_time(v))
|
109
|
+
end
|
110
|
+
|
111
|
+
# Convert a RFC 2425 date-time into an array of [year,mon,day,hour,min,sec,secfrac,timezone]
|
112
|
+
def self.decode_date_time(v) # :nodoc:
|
113
|
+
unless match = %r{^\s*#{Bnf::DATE}T#{Bnf::TIME}\s*$}.match(v)
|
114
|
+
raise Vcard::InvalidEncodingError, "date-time '#{v}' not valid"
|
115
|
+
end
|
116
|
+
year, month, day, hour, min, sec, secfrac, tz = match.to_a[1..8]
|
117
|
+
|
118
|
+
[
|
119
|
+
# date
|
120
|
+
year.to_i, month.to_i, day.to_i,
|
121
|
+
# time
|
122
|
+
hour.to_i, min.to_i, sec.to_i, secfrac ? secfrac.to_f : 0, tz
|
123
|
+
]
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.decode_date_time_to_datetime(v) #:nodoc:
|
127
|
+
year, month, day, hour, min, sec = decode_date_time(v)
|
128
|
+
# TODO - DateTime understands timezones, so we could decode tz and use it.
|
129
|
+
DateTime.civil(year, month, day, hour, min, sec, 0)
|
130
|
+
end
|
131
|
+
|
132
|
+
# decode_boolean
|
133
|
+
#
|
134
|
+
# float
|
135
|
+
#
|
136
|
+
# float_list
|
137
|
+
|
138
|
+
# Convert an RFC2425 INTEGER value into an Integer
|
139
|
+
def self.decode_integer(v) # :nodoc:
|
140
|
+
unless %r{\s*#{Bnf::INTEGER}\s*}.match(v)
|
141
|
+
raise Vcard::InvalidEncodingError, "integer not valid (#{v})"
|
142
|
+
end
|
143
|
+
v.to_i
|
144
|
+
end
|
145
|
+
|
146
|
+
#
|
147
|
+
# integer_list
|
148
|
+
|
149
|
+
# Convert a RFC2425 date-list into an array of dates.
|
150
|
+
def self.decode_date_list(v) # :nodoc:
|
151
|
+
decode_list(v) do |date|
|
152
|
+
date.strip!
|
153
|
+
if date.length > 0
|
154
|
+
decode_date(date)
|
155
|
+
end
|
156
|
+
end.compact
|
157
|
+
end
|
158
|
+
|
159
|
+
# Convert a RFC 2425 time-list into an array of times.
|
160
|
+
def self.decode_time_list(v) # :nodoc:
|
161
|
+
decode_list(v) do |time|
|
162
|
+
time.strip!
|
163
|
+
if time.length > 0
|
164
|
+
decode_time(time)
|
165
|
+
end
|
166
|
+
end.compact
|
167
|
+
end
|
168
|
+
|
169
|
+
# Convert a RFC 2425 date-time-list into an array of date-times.
|
170
|
+
def self.decode_date_time_list(v) # :nodoc:
|
171
|
+
decode_list(v) do |datetime|
|
172
|
+
datetime.strip!
|
173
|
+
if datetime.length > 0
|
174
|
+
decode_date_time(datetime)
|
175
|
+
end
|
176
|
+
end.compact
|
177
|
+
end
|
178
|
+
|
179
|
+
# Convert RFC 2425 text into a String.
|
180
|
+
# \\ -> \
|
181
|
+
# \n -> NL
|
182
|
+
# \N -> NL
|
183
|
+
# \, -> ,
|
184
|
+
# \; -> ;
|
185
|
+
#
|
186
|
+
# I've seen double-quote escaped by iCal.app. Hmm. Ok, if you aren't supposed
|
187
|
+
# to escape anything but the above, everything else is ambiguous, so I'll
|
188
|
+
# just support it.
|
189
|
+
def self.decode_text(v) # :nodoc:
|
190
|
+
# FIXME - I think this should trim leading and trailing space
|
191
|
+
v.gsub(/\\(.)/) do
|
192
|
+
case $1
|
193
|
+
when "n", "N"
|
194
|
+
"\n"
|
195
|
+
else
|
196
|
+
$1
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.encode_text(v) #:nodoc:
|
202
|
+
v.to_str.gsub(/([\\,;\n])/) { $1 == "\n" ? "\\n" : "\\"+$1 }
|
203
|
+
end
|
204
|
+
|
205
|
+
# v is an Array of String, or just a single String
|
206
|
+
def self.encode_text_list(v, sep = ",") #:nodoc:
|
207
|
+
begin
|
208
|
+
v.to_ary.map{ |t| encode_text(t) }.join(sep)
|
209
|
+
rescue
|
210
|
+
encode_text(v)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# Convert a +sep+-seperated list of TEXT values into an array of values.
|
215
|
+
def self.decode_text_list(value, sep = ",") # :nodoc:
|
216
|
+
# Need to do in two stages, as best I can find.
|
217
|
+
list = value.scan(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)#{sep}/).map do |v|
|
218
|
+
decode_text(v.first)
|
219
|
+
end
|
220
|
+
if value.match(/([^#{sep}\\]*(?:\\.[^#{sep}\\]*)*)$/)
|
221
|
+
list << $1
|
222
|
+
end
|
223
|
+
list
|
224
|
+
end
|
225
|
+
|
226
|
+
# param-value = paramtext / quoted-string
|
227
|
+
# paramtext = *SAFE-CHAR
|
228
|
+
# quoted-string = DQUOTE *QSAFE-CHAR DQUOTE
|
229
|
+
def self.encode_paramtext(value)
|
230
|
+
case value
|
231
|
+
when %r{\A#{Bnf::SAFECHAR}*\z}
|
232
|
+
value
|
233
|
+
else
|
234
|
+
raise Vcard::Unencodable, "paramtext #{value.inspect}"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def self.encode_paramvalue(value)
|
239
|
+
case value
|
240
|
+
when %r{\A#{Bnf::SAFECHAR}*\z}
|
241
|
+
value
|
242
|
+
when %r{\A#{Bnf::QSAFECHAR}*\z}
|
243
|
+
'"' + value + '"'
|
244
|
+
else
|
245
|
+
raise Vcard::Unencodable, "param-value #{value.inspect}"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
|
250
|
+
# Unfold the lines in +card+, then return an array of one Field object per
|
251
|
+
# line.
|
252
|
+
def self.decode(card) #:nodoc:
|
253
|
+
unfold(card).collect { |line| DirectoryInfo::Field.decode(line) }
|
254
|
+
end
|
255
|
+
|
256
|
+
|
257
|
+
# Expand an array of fields into its syntactic entities. Each entity is a sequence
|
258
|
+
# of fields where the sequences is delimited by a BEGIN/END field. Since
|
259
|
+
# BEGIN/END delimited entities can be nested, we build a tree. Each entry in
|
260
|
+
# the array is either a Field or an array of entries (where each entry is
|
261
|
+
# either a Field, or an array of entries...).
|
262
|
+
def self.expand(src) #:nodoc:
|
263
|
+
# output array to expand the src to
|
264
|
+
dst = []
|
265
|
+
# stack used to track our nesting level, as we see begin/end we start a
|
266
|
+
# new/finish the current entity, and push/pop that entity from the stack
|
267
|
+
current = [ dst ]
|
268
|
+
|
269
|
+
for f in src
|
270
|
+
if f.name? "BEGIN"
|
271
|
+
e = [ f ]
|
272
|
+
|
273
|
+
current.last.push(e)
|
274
|
+
current.push(e)
|
275
|
+
|
276
|
+
elsif f.name? "END"
|
277
|
+
current.last.push(f)
|
278
|
+
|
279
|
+
unless current.last.first.value? current.last.last.value
|
280
|
+
raise "BEGIN/END mismatch (#{current.last.first.value} != #{current.last.last.value})"
|
281
|
+
end
|
282
|
+
|
283
|
+
current.pop
|
284
|
+
|
285
|
+
else
|
286
|
+
current.last.push(f)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
dst
|
291
|
+
end
|
292
|
+
|
293
|
+
# Split an array into an array of all the fields at the outer level, and
|
294
|
+
# an array of all the inner arrays of fields. Return the array [outer,
|
295
|
+
# inner].
|
296
|
+
def self.outer_inner(fields) #:nodoc:
|
297
|
+
# TODO - use Enumerable#partition
|
298
|
+
# seperate into the outer-level fields, and the arrays of component
|
299
|
+
# fields
|
300
|
+
outer = []
|
301
|
+
inner = []
|
302
|
+
fields.each do |line|
|
303
|
+
case line
|
304
|
+
when Array; inner << line
|
305
|
+
else; outer << line
|
306
|
+
end
|
307
|
+
end
|
308
|
+
return outer, inner
|
309
|
+
end
|
310
|
+
end
|