mumboe-vpim 0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (69) hide show
  1. data/CHANGES +510 -0
  2. data/COPYING +58 -0
  3. data/README +185 -0
  4. data/lib/vpim/address.rb +219 -0
  5. data/lib/vpim/agent/atomize.rb +104 -0
  6. data/lib/vpim/agent/base.rb +73 -0
  7. data/lib/vpim/agent/calendars.rb +173 -0
  8. data/lib/vpim/agent/handler.rb +26 -0
  9. data/lib/vpim/agent/ics.rb +161 -0
  10. data/lib/vpim/attachment.rb +102 -0
  11. data/lib/vpim/date.rb +222 -0
  12. data/lib/vpim/dirinfo.rb +277 -0
  13. data/lib/vpim/duration.rb +119 -0
  14. data/lib/vpim/enumerator.rb +32 -0
  15. data/lib/vpim/field.rb +614 -0
  16. data/lib/vpim/icalendar.rb +384 -0
  17. data/lib/vpim/maker/vcard.rb +16 -0
  18. data/lib/vpim/property/base.rb +193 -0
  19. data/lib/vpim/property/common.rb +315 -0
  20. data/lib/vpim/property/location.rb +38 -0
  21. data/lib/vpim/property/priority.rb +43 -0
  22. data/lib/vpim/property/recurrence.rb +69 -0
  23. data/lib/vpim/property/resources.rb +24 -0
  24. data/lib/vpim/repo.rb +261 -0
  25. data/lib/vpim/rfc2425.rb +367 -0
  26. data/lib/vpim/rrule.rb +591 -0
  27. data/lib/vpim/time.rb +40 -0
  28. data/lib/vpim/vcard.rb +1456 -0
  29. data/lib/vpim/version.rb +18 -0
  30. data/lib/vpim/vevent.rb +187 -0
  31. data/lib/vpim/view.rb +90 -0
  32. data/lib/vpim/vjournal.rb +58 -0
  33. data/lib/vpim/vpim.rb +65 -0
  34. data/lib/vpim/vtodo.rb +103 -0
  35. data/lib/vpim.rb +13 -0
  36. data/samples/README.mutt +93 -0
  37. data/samples/ab-query.rb +57 -0
  38. data/samples/agent.ru +10 -0
  39. data/samples/cmd-itip.rb +156 -0
  40. data/samples/ex_cpvcard.rb +55 -0
  41. data/samples/ex_get_vcard_photo.rb +22 -0
  42. data/samples/ex_mkv21vcard.rb +34 -0
  43. data/samples/ex_mkvcard.rb +64 -0
  44. data/samples/ex_mkyourown.rb +29 -0
  45. data/samples/ics-dump.rb +210 -0
  46. data/samples/ics-to-rss.rb +84 -0
  47. data/samples/mutt-aliases-to-vcf.rb +45 -0
  48. data/samples/osx-wrappers.rb +86 -0
  49. data/samples/reminder.rb +209 -0
  50. data/samples/rrule.rb +71 -0
  51. data/samples/tabbed-file-to-vcf.rb +390 -0
  52. data/samples/vcf-dump.rb +86 -0
  53. data/samples/vcf-lines.rb +61 -0
  54. data/samples/vcf-to-ics.rb +22 -0
  55. data/samples/vcf-to-mutt.rb +121 -0
  56. data/test/test_agent_atomize.rb +84 -0
  57. data/test/test_agent_calendars.rb +128 -0
  58. data/test/test_agent_ics.rb +96 -0
  59. data/test/test_all.rb +17 -0
  60. data/test/test_date.rb +120 -0
  61. data/test/test_dur.rb +41 -0
  62. data/test/test_field.rb +156 -0
  63. data/test/test_ical.rb +437 -0
  64. data/test/test_misc.rb +13 -0
  65. data/test/test_repo.rb +129 -0
  66. data/test/test_rrule.rb +1030 -0
  67. data/test/test_vcard.rb +973 -0
  68. data/test/test_view.rb +79 -0
  69. metadata +140 -0
data/lib/vpim/repo.rb ADDED
@@ -0,0 +1,261 @@
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
+ require "net/http"
11
+
12
+ require 'plist'
13
+
14
+ require 'vpim/icalendar'
15
+ require 'vpim/duration'
16
+
17
+ module Vpim
18
+ # A Repo is a representation of a calendar repository.
19
+ #
20
+ # Currently supported repository types are:
21
+ # - Repo::Apple3, an Apple iCal3 repository.
22
+ # - Repo::Directory, a directory hierarchy containing .ics files
23
+ # - Repo::Uri, a URI that identifies a single iCalendar
24
+ #
25
+ # All repository types support at least the methods of Repo, and all
26
+ # repositories return calendars that support at least the methods of
27
+ # Repo::Calendar.
28
+ class Repo
29
+ include Enumerable
30
+
31
+ # Open a repository at location +where+.
32
+ def initialize(where)
33
+ end
34
+
35
+ # Enumerate the calendars in the repository.
36
+ def each #:yield: calendar
37
+ end
38
+
39
+ # A calendar abstraction. It models a calendar in a calendar repository
40
+ # that may not be an iCalendar.
41
+ #
42
+ # It has methods that behave identically to Icalendar, but it also has
43
+ # methods like name and displayed that are not present in an iCalendar.
44
+ class Calendar
45
+ include Enumerable
46
+
47
+ # The calendar name.
48
+ def name
49
+ end
50
+
51
+ # Whether a calendar should be displayed.
52
+ #
53
+ # TODO - should be #displayed?
54
+ def displayed
55
+ end
56
+
57
+ # Encode into iCalendar format.
58
+ def encode
59
+ end
60
+
61
+ # Enumerate the components in the calendar, both todos and events, or
62
+ # the specified klass. Like Icalendar#each()
63
+ def each(klass=nil, &block) #:yield: component
64
+ end
65
+
66
+ # Enumerate the events in the calendar.
67
+ def events(&block) #:yield: Vevent
68
+ each(Vpim::Icalendar::Vevent, &block)
69
+ end
70
+
71
+ # Enumerate the todos in the calendar.
72
+ def todos(&block) #:yield: Vtodo
73
+ each(Vpim::Icalendar::Vtodo, &block)
74
+ end
75
+
76
+ # The method definitions are just to fool rdoc, not to be used.
77
+ %w{each name displayed encode}.each{|m| remove_method m}
78
+
79
+ def file_each(file, klass, &block) #:nodoc:
80
+ unless iterator?
81
+ return Enumerable::Enumerator.new(self, :each, klass)
82
+ end
83
+
84
+ cals = open(file) do |io|
85
+ Vpim::Icalendar.decode(io)
86
+ end
87
+
88
+ cals.each do |cal|
89
+ cal.each(klass, &block)
90
+ end
91
+ self
92
+ end
93
+ end
94
+ end
95
+
96
+ class Repo
97
+ include Enumerable
98
+
99
+ # An Apple iCal version 3 repository.
100
+ class Apple3 < Repo
101
+ def initialize(where = "~/Library/Calendars")
102
+ @where = where.to_str
103
+ end
104
+
105
+ def each #:nodoc:
106
+ Dir[ File.expand_path(@where + "/**/*.calendar") ].each do |dir|
107
+ yield Calendar.new(dir)
108
+ end
109
+ self
110
+ end
111
+
112
+ class Calendar < Repo::Calendar
113
+ def initialize(dir) # :nodoc:
114
+ @dir = dir
115
+ end
116
+
117
+ def plist(key) #:nodoc:
118
+ Plist::parse_xml( @dir + "/Info.plist")[key]
119
+ end
120
+
121
+ def name #:nodoc:
122
+ plist "Title"
123
+ end
124
+
125
+ def displayed #:nodoc:
126
+ 1 == plist("Checked")
127
+ end
128
+
129
+ def each(klass=nil, &block) #:nodoc:
130
+ unless iterator?
131
+ return Enumerable::Enumerator.new(self, :each, klass)
132
+ end
133
+ Dir[ @dir + "/Events/*.ics" ].map do |ics|
134
+ file_each(ics, klass, &block)
135
+ end
136
+ self
137
+ end
138
+
139
+ def encode #:nodoc:
140
+ Icalendar.create2 do |cal|
141
+ each{|c| cal << c}
142
+ end.encode
143
+ end
144
+ end
145
+
146
+ end
147
+
148
+ class Directory < Repo
149
+ class Calendar < Repo::Calendar
150
+ def initialize(file) #:nodoc:
151
+ @file = file
152
+ end
153
+
154
+ def name #:nodoc:
155
+ File.basename(@file)
156
+ end
157
+
158
+ def displayed #:nodoc:
159
+ true
160
+ end
161
+
162
+ def each(klass, &block) #:nodoc:
163
+ file_each(@file, klass, &block)
164
+ end
165
+
166
+ def encode #:nodoc:
167
+ open(@file, "r"){|f| f.read}
168
+ end
169
+
170
+ end
171
+
172
+ def initialize(where = ".")
173
+ @where = where.to_str
174
+ end
175
+
176
+ def each #:nodoc:
177
+ Dir[ File.expand_path(@where + "/**/*.ics") ].each do |file|
178
+ yield Calendar.new(file)
179
+ end
180
+ self
181
+ end
182
+ end
183
+
184
+ class Uri < Repo
185
+ def self.uri_check(uri)
186
+ uri = case uri
187
+ when URI
188
+ uri
189
+ else
190
+ begin
191
+ URI.parse(uri.sub(/^webcal:/, "http:"))
192
+ rescue URI::InvalidURIError => e
193
+ raise ArgumentError, "Invalid URI for #{uri.inspect} - #{e.to_s}"
194
+ end
195
+ end
196
+ unless uri.scheme == "http"
197
+ raise ArgumentError, "Unsupported URI scheme for #{uri.inspect}"
198
+ end
199
+ uri
200
+ end
201
+
202
+ class Calendar < Repo::Calendar
203
+ def body
204
+ end
205
+
206
+ def initialize(uri) #:nodoc:
207
+ @uri = Uri.uri_check(uri)
208
+ end
209
+
210
+ def name #:nodoc:
211
+ @uri.to_s
212
+ end
213
+
214
+ def displayed #:nodoc:
215
+ true
216
+ end
217
+
218
+ def each(klass, &block) #:nodoc:
219
+ unless iterator?
220
+ return Enumerable::Enumerator.new(self, :each, klass)
221
+ end
222
+
223
+ cals = Vpim::Icalendar.decode(encode)
224
+
225
+ cals.each do |cal|
226
+ cal.each(klass, &block)
227
+ end
228
+ self
229
+ end
230
+
231
+ def encode #:nodoc:
232
+ Net::HTTP.get_response(@uri) do |result|
233
+ accum = ""
234
+ =begin
235
+ better to let this pass up as an invalid encoding error
236
+ if result.code != "200"
237
+ raise StandardError,
238
+ "HTTP GET of #{@uri.to_s.inspect} failed with #{result.code} #{result.error_type}"
239
+ end
240
+ =end
241
+ result.read_body do |chunk|
242
+ accum << chunk
243
+ end
244
+ return accum
245
+ end
246
+ end
247
+
248
+ end
249
+
250
+ def initialize(where)
251
+ @where = Uri.uri_check(where)
252
+ end
253
+
254
+ def each #:nodoc:
255
+ yield Calendar.new(@where)
256
+ self
257
+ end
258
+ end
259
+ end
260
+ end
261
+
@@ -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_line 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
+