mumboe-vpim 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 (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
+