vpim2 0.0.1

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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGES +504 -0
  3. data/COPYING +58 -0
  4. data/README +182 -0
  5. data/lib/atom.rb +728 -0
  6. data/lib/plist.rb +22 -0
  7. data/lib/vpim.rb +13 -0
  8. data/lib/vpim/address.rb +219 -0
  9. data/lib/vpim/attachment.rb +102 -0
  10. data/lib/vpim/date.rb +222 -0
  11. data/lib/vpim/dirinfo.rb +277 -0
  12. data/lib/vpim/duration.rb +119 -0
  13. data/lib/vpim/enumerator.rb +32 -0
  14. data/lib/vpim/field.rb +614 -0
  15. data/lib/vpim/icalendar.rb +381 -0
  16. data/lib/vpim/maker/vcard.rb +16 -0
  17. data/lib/vpim/property/base.rb +193 -0
  18. data/lib/vpim/property/common.rb +315 -0
  19. data/lib/vpim/property/location.rb +38 -0
  20. data/lib/vpim/property/priority.rb +43 -0
  21. data/lib/vpim/property/recurrence.rb +69 -0
  22. data/lib/vpim/property/resources.rb +24 -0
  23. data/lib/vpim/repo.rb +181 -0
  24. data/lib/vpim/rfc2425.rb +367 -0
  25. data/lib/vpim/rrule.rb +591 -0
  26. data/lib/vpim/vcard.rb +1430 -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 +117 -0
@@ -0,0 +1,277 @@
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/enumerator'
10
+ require 'vpim/field'
11
+ require 'vpim/rfc2425'
12
+ require 'vpim/vpim'
13
+
14
+ module Vpim
15
+ # An RFC 2425 directory info object.
16
+ #
17
+ # A directory information object is a sequence of fields. The basic
18
+ # structure of the object, and the way in which it is broken into fields
19
+ # is common to all profiles of the directory info type.
20
+ #
21
+ # A vCard, for example, is a specialization of a directory info object.
22
+ #
23
+ # - [RFC2425] the directory information framework (ftp://ftp.ietf.org/rfc/rfc2425.txt)
24
+ #
25
+ # Here's an example of encoding a simple vCard using the low-level APIs:
26
+ #
27
+ # card = Vpim::Vcard.create
28
+ # card << Vpim::DirectoryInfo::Field.create('EMAIL', 'user.name@example.com', 'TYPE' => 'INTERNET' )
29
+ # card << Vpim::DirectoryInfo::Field.create('URL', 'http://www.example.com/user' )
30
+ # card << Vpim::DirectoryInfo::Field.create('FN', 'User Name' )
31
+ # puts card.to_s
32
+ #
33
+ # Don't do it like that, use Vpim::Vcard::Maker.
34
+ class DirectoryInfo
35
+ include Enumerable
36
+
37
+ private_class_method :new
38
+
39
+ # Initialize a DirectoryInfo object from +fields+. If +profile+ is
40
+ # specified, check the BEGIN/END fields.
41
+ def initialize(fields, profile = nil) #:nodoc:
42
+ if fields.detect { |f| ! f.kind_of? DirectoryInfo::Field }
43
+ raise ArgumentError, 'fields must be an array of DirectoryInfo::Field objects'
44
+ end
45
+
46
+ @string = nil # this is used as a flag to indicate that recoding will be necessary
47
+ @fields = fields
48
+
49
+ check_begin_end(profile) if profile
50
+ end
51
+
52
+ # Decode +card+ into a DirectoryInfo object.
53
+ #
54
+ # +card+ may either be a something that is convertible to a string using
55
+ # #to_str or an Array of objects that can be joined into a string using
56
+ # #join("\n"), or an IO object (which will be read to end-of-file).
57
+ #
58
+ # The lines in the string may be delimited using IETF (CRLF) or Unix (LF) conventions.
59
+ #
60
+ # A DirectoryInfo is mutable, you can add new fields to it, see
61
+ # Vpim::DirectoryInfo::Field#create() for how to create a new Field.
62
+ #
63
+ # TODO: I don't believe this is ever used, maybe I can remove it.
64
+ def DirectoryInfo.decode(card) #:nodoc:
65
+ if card.respond_to? :to_str
66
+ string = card.to_str
67
+ elsif card.kind_of? Array
68
+ string = card.join("\n")
69
+ elsif card.kind_of? IO
70
+ string = card.read(nil)
71
+ else
72
+ raise ArgumentError, "DirectoryInfo cannot be created from a #{card.type}"
73
+ end
74
+
75
+ fields = Vpim.decode(string)
76
+
77
+ new(fields)
78
+ end
79
+
80
+ # Create a new DirectoryInfo object. The +fields+ are an optional array of
81
+ # DirectoryInfo::Field objects to add to the new object, between the
82
+ # BEGIN/END. If the +profile+ string is not nil, then it is the name of
83
+ # the directory info profile, and the BEGIN:+profile+/END:+profile+ fields
84
+ # will be added.
85
+ #
86
+ # A DirectoryInfo is mutable, you can add new fields to it using #push(),
87
+ # and see Field#create().
88
+ def DirectoryInfo.create(fields = [], profile = nil)
89
+
90
+ if profile
91
+ p = profile.to_str
92
+ f = [ Field.create('BEGIN', p) ]
93
+ f.concat fields
94
+ f.push Field.create('END', p)
95
+ fields = f
96
+ end
97
+
98
+ new(fields, profile)
99
+ end
100
+
101
+ # The first field named +name+, or nil if no
102
+ # match is found.
103
+ def field(name)
104
+ enum_by_name(name).each { |f| return f }
105
+ nil
106
+ end
107
+
108
+ # The value of the first field named +name+, or nil if no
109
+ # match is found.
110
+ def [](name)
111
+ enum_by_name(name).each { |f| return f.value if f.value != ''}
112
+ enum_by_name(name).each { |f| return f.value }
113
+ nil
114
+ end
115
+
116
+ # An array of all the values of fields named +name+, converted to text
117
+ # (using Field#to_text()).
118
+ #
119
+ # TODO - call this #texts(), as in the plural?
120
+ def text(name)
121
+ accum = []
122
+ each do |f|
123
+ if f.name? name
124
+ accum << f.to_text
125
+ end
126
+ end
127
+ accum
128
+ end
129
+
130
+ # Array of all the Field#group()s.
131
+ def groups
132
+ @fields.collect { |f| f.group } .compact.uniq
133
+ end
134
+
135
+ # All fields, frozen.
136
+ def fields #:nodoc:
137
+ @fields.dup.freeze
138
+ end
139
+
140
+ # Yields for each Field for which +cond+.call(field) is true. The
141
+ # (default) +cond+ of nil is considered true for all fields, so
142
+ # this acts like a normal #each() when called with no arguments.
143
+ def each(cond = nil) # :yields: Field
144
+ @fields.each do |field|
145
+ if(cond == nil || cond.call(field))
146
+ yield field
147
+ end
148
+ end
149
+ self
150
+ end
151
+
152
+ # Returns an Enumerator for each Field for which #name?(+name+) is true.
153
+ #
154
+ # An Enumerator supports all the methods of Enumerable, so it allows iteration,
155
+ # collection, mapping, etc.
156
+ #
157
+ # Examples:
158
+ #
159
+ # Print all the nicknames in a card:
160
+ #
161
+ # card.enum_by_name('NICKNAME') { |f| puts f.value }
162
+ #
163
+ # Print an Array of the preferred email addresses in the card:
164
+ #
165
+ # pref_emails = card.enum_by_name('EMAIL').select { |f| f.pref? }
166
+ def enum_by_name(name)
167
+ Enumerator.new(self, Proc.new { |field| field.name?(name) })
168
+ end
169
+
170
+ # Returns an Enumerator for each Field for which #group?(+group+) is true.
171
+ #
172
+ # For example, to print all the fields, sorted by group, you could do:
173
+ #
174
+ # card.groups.sort.each do |group|
175
+ # card.enum_by_group(group).each do |field|
176
+ # puts "#{group} -> #{field.name}"
177
+ # end
178
+ # end
179
+ #
180
+ # or to get an array of all the fields in group 'AGROUP', you could do:
181
+ #
182
+ # card.enum_by_group('AGROUP').to_a
183
+ def enum_by_group(group)
184
+ Enumerator.new(self, Proc.new { |field| field.group?(group) })
185
+ end
186
+
187
+ # Returns an Enumerator for each Field for which +cond+.call(field) is true.
188
+ def enum_by_cond(cond)
189
+ Enumerator.new(self, cond )
190
+ end
191
+
192
+ # Force card to be reencoded from the fields.
193
+ def dirty #:nodoc:
194
+ #string = nil
195
+ end
196
+
197
+ # Append +field+ to the fields. Note that it won't be literally appended
198
+ # to the fields, it will be inserted before the closing END field.
199
+ def push(field)
200
+ dirty
201
+ @fields[-1,0] = field
202
+ self
203
+ end
204
+
205
+ alias << push
206
+
207
+ # Push +field+ onto the fields, unless there is already a field
208
+ # with this name.
209
+ def push_unique(field)
210
+ push(field) unless @fields.detect { |f| f.name? field.name }
211
+ self
212
+ end
213
+
214
+ # Append +field+ to the end of all the fields. This isn't usually what you
215
+ # want to do, usually a DirectoryInfo's first and last fields are a
216
+ # BEGIN/END pair, see #push().
217
+ def push_end(field)
218
+ @fields << field
219
+ self
220
+ end
221
+
222
+ # Delete +field+.
223
+ #
224
+ # Warning: You can't delete BEGIN: or END: fields, but other
225
+ # profile-specific fields can be deleted, including mandatory ones. For
226
+ # vCards in particular, in order to avoid destroying them, I suggest
227
+ # creating a new Vcard, and copying over all the fields that you still
228
+ # want, rather than using #delete. This is easy with Vcard::Maker#copy, see
229
+ # the Vcard::Maker examples.
230
+ def delete(field)
231
+ case
232
+ when field.name?('BEGIN'), field.name?('END')
233
+ raise ArgumentError, 'Cannot delete BEGIN or END fields.'
234
+ else
235
+ @fields.delete field
236
+ end
237
+
238
+ self
239
+ end
240
+
241
+ # The string encoding of the DirectoryInfo. See Field#encode for information
242
+ # about the width parameter.
243
+ def encode(width=nil)
244
+ unless @string
245
+ @string = @fields.collect { |f| f.encode(width) } . join ""
246
+ end
247
+ @string
248
+ end
249
+
250
+ alias to_s encode
251
+
252
+ # Check that the DirectoryInfo object is correctly delimited by a BEGIN
253
+ # and END, that their profile values match, and if +profile+ is specified, that
254
+ # they are the specified profile.
255
+ def check_begin_end(profile=nil) #:nodoc:
256
+ unless @fields.first
257
+ raise "No fields to check"
258
+ end
259
+ unless @fields.first.name? 'BEGIN'
260
+ raise "Needs BEGIN, found: #{@fields.first.encode nil}"
261
+ end
262
+ unless @fields.last.name? 'END'
263
+ raise "Needs END, found: #{@fields.last.encode nil}"
264
+ end
265
+ unless @fields.last.value? @fields.first.value
266
+ raise "BEGIN/END mismatch: (#{@fields.first.value} != #{@fields.last.value}"
267
+ end
268
+ if profile
269
+ if ! @fields.first.value? profile
270
+ raise "Mismatched profile"
271
+ end
272
+ end
273
+ true
274
+ end
275
+ end
276
+ end
277
+
@@ -0,0 +1,119 @@
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 Duration
11
+ SECS_HOUR = 60 * 60
12
+ SECS_DAY = 24 * SECS_HOUR
13
+ MINS_HOUR = 60
14
+
15
+ # Initialize from a number of seconds.
16
+ def initialize(secs)
17
+ @secs = secs
18
+ end
19
+
20
+ def Duration.secs(secs)
21
+ Duration.new(secs)
22
+ end
23
+
24
+ def Duration.mins(mins)
25
+ Duration.new(mins * 60)
26
+ end
27
+
28
+ def Duration.hours(hours)
29
+ Duration.new(hours * SECS_HOUR)
30
+ end
31
+
32
+ def Duration.days(days)
33
+ Duration.new(days * SECS_DAY)
34
+ end
35
+
36
+ def secs
37
+ @secs
38
+ end
39
+
40
+ def mins
41
+ (@secs/60).to_i
42
+ end
43
+
44
+ def hours
45
+ (@secs/SECS_HOUR).to_i
46
+ end
47
+
48
+ def days
49
+ (@secs/SECS_DAY).to_i
50
+ end
51
+
52
+ def weeks
53
+ (days/7).to_i
54
+ end
55
+
56
+ def by_hours
57
+ [ hours, mins % MINS_HOUR, secs % 60]
58
+ end
59
+
60
+ def by_days
61
+ [ days, hours % 24, mins % MINS_HOUR, secs % 60]
62
+ end
63
+
64
+ def to_a
65
+ by_days
66
+ end
67
+
68
+ def to_s
69
+ Duration.as_str(self.to_a)
70
+ end
71
+
72
+ def Duration.as_str(arr)
73
+ s = ""
74
+ case arr.length
75
+ when 4
76
+ if arr[0] > 0
77
+ s << "#{arr[0]} days"
78
+ end
79
+ if arr[1] > 0
80
+ if s.length > 0
81
+ s << ', '
82
+ end
83
+ s << "#{arr[1]} hours"
84
+ end
85
+ if arr[2] > 0
86
+ if s.length > 0
87
+ s << ', '
88
+ end
89
+ s << "#{arr[2]} mins"
90
+ end
91
+ if arr[3] > 0
92
+ if s.length > 0
93
+ s << ', '
94
+ end
95
+ s << "#{arr[3]} secs"
96
+ end
97
+ when 3
98
+ if arr[0] > 0
99
+ s << "#{arr[0]} hours"
100
+ end
101
+ if arr[1] > 0
102
+ if s.length > 0
103
+ s << ', '
104
+ end
105
+ s << "#{arr[1]} mins"
106
+ end
107
+ if arr[2] > 0
108
+ if s.length > 0
109
+ s << ', '
110
+ end
111
+ s << "#{arr[2]} secs"
112
+ end
113
+ end
114
+
115
+ s
116
+ end
117
+ end
118
+ end
119
+
@@ -0,0 +1,32 @@
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
+ module Vpim
12
+ # This is a way for an object to have multiple ways of being enumerated via
13
+ # argument to it's #each() method. An Enumerator mixes in Enumerable, so the
14
+ # standard APIs such as Enumerable#map(), Enumerable#to_a(), and
15
+ # Enumerable#find_all() can be used on it.
16
+ #
17
+ # TODO since 1.8, this is part of the standard library, I should rewrite vPim
18
+ # so this can be removed.
19
+ class Enumerator
20
+ include Enumerable
21
+
22
+ def initialize(obj, *args)
23
+ @obj = obj
24
+ @args = args
25
+ end
26
+
27
+ def each(&block)
28
+ @obj.each(*@args, &block)
29
+ end
30
+ end
31
+ end
32
+
data/lib/vpim/field.rb ADDED
@@ -0,0 +1,614 @@
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/rfc2425'
10
+ require 'vpim/vpim'
11
+ require 'date'
12
+
13
+ module Vpim
14
+
15
+ class DirectoryInfo
16
+
17
+ # A field in a directory info object.
18
+ class Field
19
+ # TODO
20
+ # - Field should know which param values and field values are
21
+ # case-insensitive, configurably, so it can down case them
22
+ # - perhaps should have pvalue_set/del/add, perhaps case-insensitive, or
23
+ # pvalue_iset/idel/iadd, where set sets them all, add adds if not present,
24
+ # and del deletes any that are present
25
+ # - I really, really, need a case-insensitive string...
26
+ # - should allow nil as a field value, its not the same as '', if there is
27
+ # more than one pvalue, the empty string will show up. This isn't strictly
28
+ # disallowed, but its odd. Should also strip empty strings on decoding, if
29
+ # I don't already.
30
+ private_class_method :new
31
+
32
+ def Field.create_array(fields)
33
+ case fields
34
+ when Hash
35
+ fields.map do |name,value|
36
+ DirectoryInfo::Field.create( name, value )
37
+ end
38
+ else
39
+ fields.to_ary
40
+ end
41
+ end
42
+
43
+ # Encode a field.
44
+ def Field.encode0(group, name, params={}, value='') # :nodoc:
45
+ line = ""
46
+
47
+ # A reminder of the line format:
48
+ # [<group>.]<name>;<pname>=<pvalue>,<pvalue>:<value>
49
+
50
+ if group
51
+ line << group << '.'
52
+ end
53
+
54
+ line << name
55
+
56
+ params.each do |pname, pvalues|
57
+
58
+ unless pvalues.respond_to? :to_ary
59
+ pvalues = [ pvalues ]
60
+ end
61
+
62
+ line << ';' << pname << '='
63
+
64
+ sep = "" # set to ',' after one pvalue has been appended
65
+
66
+ pvalues.each do |pvalue|
67
+ # check if we need to do any encoding
68
+ if Vpim::Methods.casecmp?(pname, 'ENCODING') && pvalue == :b64
69
+ pvalue = 'B' # the RFC definition of the base64 param value
70
+ value = [ value.to_str ].pack('m').gsub("\n", '')
71
+ end
72
+
73
+ line << sep << pvalue
74
+ sep =",";
75
+ end
76
+ end
77
+
78
+ line << ':'
79
+
80
+ line << Field.value_str(value)
81
+
82
+ line
83
+ end
84
+
85
+ def Field.value_str(value) # :nodoc:
86
+ line = ''
87
+ case value
88
+ when Date
89
+ line << Vpim.encode_date(value)
90
+
91
+ when Time #, DateTime
92
+ line << Vpim.encode_date_time(value)
93
+
94
+ when Array
95
+ line << value.map { |v| Field.value_str(v) }.join(';')
96
+
97
+ when Symbol
98
+ line << value
99
+
100
+ else
101
+ # FIXME - somewhere along here, values with special chars need escaping...
102
+ line << value.to_str
103
+ end
104
+ line
105
+ end
106
+
107
+ # Decode a field.
108
+ def Field.decode0(atline) # :nodoc:
109
+ unless atline =~ %r{#{Bnf::LINE}}i
110
+ raise Vpim::InvalidEncodingError, atline
111
+ end
112
+
113
+ atgroup = $1.upcase
114
+ atname = $2.upcase
115
+ paramslist = $3
116
+ atvalue = $~[-1]
117
+
118
+ # I've seen space that shouldn't be there, as in "BEGIN:VCARD ", so
119
+ # strip it. I'm not absolutely sure this is allowed... it certainly
120
+ # breaks round-trip encoding.
121
+ atvalue.strip!
122
+
123
+ if atgroup.length > 0
124
+ atgroup.chomp!('.')
125
+ else
126
+ atgroup = nil
127
+ end
128
+
129
+ atparams = {}
130
+
131
+ # Collect the params, if any.
132
+ if paramslist.size > 1
133
+
134
+ # v3.0 and v2.1 params
135
+ paramslist.scan( %r{#{Bnf::PARAM}}i ) do
136
+
137
+ # param names are case-insensitive, and multi-valued
138
+ name = $1.upcase
139
+ params = $3
140
+
141
+ # v2.1 params have no '=' sign, figure out what kind of param it
142
+ # is (either its a known encoding, or we treat it as a 'TYPE'
143
+ # param).
144
+
145
+ if $2 == ""
146
+ params = $1
147
+ case $1
148
+ when /quoted-printable/i
149
+ name = 'ENCODING'
150
+
151
+ when /base64/i
152
+ name = 'ENCODING'
153
+
154
+ else
155
+ name = 'TYPE'
156
+ end
157
+ end
158
+
159
+ # TODO - In ruby1.8 I can give an initial value to the atparams
160
+ # hash values instead of this.
161
+ unless atparams.key? name
162
+ atparams[name] = []
163
+ end
164
+
165
+ params.scan( %r{#{Bnf::PVALUE}} ) do
166
+ atparams[name] << ($1 || $2)
167
+ end
168
+ end
169
+ end
170
+
171
+ [ atgroup, atname, atparams, atvalue ]
172
+ end
173
+
174
+ def initialize(line) # :nodoc:
175
+ @line = line.to_str
176
+ @group, @name, @params, @value = Field.decode0(@line)
177
+
178
+ @params.each do |pname,pvalues|
179
+ pvalues.freeze
180
+ end
181
+ self
182
+ end
183
+
184
+ # Create a field by decoding +line+, a String which must already be
185
+ # unfolded. Decoded fields are frozen, but see #copy().
186
+ def Field.decode(line)
187
+ new(line).freeze
188
+ end
189
+
190
+ # Create a field with name +name+ (a String), value +value+ (see below),
191
+ # and optional parameters, +params+. +params+ is a hash of the parameter
192
+ # name (a String) to either a single string or symbol, or an array of
193
+ # strings and symbols (parameters can be multi-valued).
194
+ #
195
+ # If 'ENCODING' => :b64 is specified as a parameter, the value will be
196
+ # base-64 encoded. If it's already base-64 encoded, then use String
197
+ # values ('ENCODING' => 'B'), and no further encoding will be done by
198
+ # this routine.
199
+ #
200
+ # Currently handled value types are:
201
+ # - Time, encoded as a date-time value
202
+ # - Date, encoded as a date value
203
+ # - String, encoded directly
204
+ # - Array of String, concatentated with ';' between them.
205
+ #
206
+ # TODO - need a way to encode String values as TEXT, at least optionally,
207
+ # so as to escape special chars, etc.
208
+ def Field.create(name, value="", params={})
209
+ line = Field.encode0(nil, name, params, value)
210
+
211
+ begin
212
+ new(line)
213
+ rescue Vpim::InvalidEncodingError => e
214
+ raise ArgumentError, e.to_s
215
+ end
216
+ end
217
+
218
+ # Create a copy of Field. If the original Field was frozen, this one
219
+ # won't be.
220
+ def copy
221
+ Marshal.load(Marshal.dump(self))
222
+ end
223
+
224
+ # The String encoding of the Field. The String will be wrapped to a
225
+ # maximum line width of +width+, where +0+ means no wrapping, and nil is
226
+ # to accept the default wrapping (75, recommended by RFC2425).
227
+ #
228
+ # Note: AddressBook.app 3.0.3 neither understands to unwrap lines when it
229
+ # imports vCards (it treats them as raw new-line characters), nor wraps
230
+ # long lines on export. This is mostly a cosmetic problem, but wrapping
231
+ # can be disabled by setting width to +0+, if desired.
232
+ #
233
+ # FIXME - breaks round-trip encoding, need to change this to not wrap
234
+ # fields that are already wrapped.
235
+ def encode(width=nil)
236
+ width = 75 unless width
237
+ l = @line
238
+ # Wrap to width, unless width is zero.
239
+ if width > 0
240
+ l = l.gsub(/.{#{width},#{width}}/) { |m| m + "\n " }
241
+ end
242
+ # Make sure it's terminated with no more than a single NL.
243
+ l.gsub(/\s*\z/, '') + "\n"
244
+ end
245
+
246
+ alias to_s encode
247
+
248
+ # The name.
249
+ def name
250
+ @name
251
+ end
252
+
253
+ # The group, if present, or nil if not present.
254
+ def group
255
+ @group
256
+ end
257
+
258
+ # An Array of all the param names.
259
+ def pnames
260
+ @params.keys
261
+ end
262
+
263
+ # FIXME - remove my own uses of #params
264
+ alias params pnames # :nodoc:
265
+
266
+ # The first value of the param +name+, nil if there is no such param,
267
+ # the param has no value, or the first param value is zero-length.
268
+ def pvalue(name)
269
+ v = pvalues( name )
270
+ if v
271
+ v = v.first
272
+ end
273
+ if v
274
+ v = nil unless v.length > 0
275
+ end
276
+ v
277
+ end
278
+
279
+ # The Array of all values of the param +name+, nil if there is no such
280
+ # param, [] if the param has no values. If the Field isn't frozen, the
281
+ # Array is mutable.
282
+ def pvalues(name)
283
+ @params[name.upcase]
284
+ end
285
+
286
+ # FIXME - remove my own uses of #param
287
+ alias param pvalues # :nodoc:
288
+
289
+ alias [] pvalues
290
+
291
+ # Yield once for each param, +name+ is the parameter name, +value+ is an
292
+ # array of the parameter values.
293
+ def each_param(&block) #:yield: name, value
294
+ if @params
295
+ @params.each(&block)
296
+ end
297
+ end
298
+
299
+ # The decoded value.
300
+ #
301
+ # The encoding specified by the #encoding, if any, is stripped.
302
+ #
303
+ # Note: Both the RFC 2425 encoding param ("b", meaning base-64) and the
304
+ # vCard 2.1 encoding params ("base64", "quoted-printable", "8bit", and
305
+ # "7bit") are supported.
306
+ #
307
+ # FIXME:
308
+ # - should use the VALUE parameter
309
+ # - should also take a default value type, so it can be converted
310
+ # if VALUE parameter is not present.
311
+ def value
312
+ case encoding
313
+ when nil, '8BIT', '7BIT' then @value
314
+
315
+ # Hack - if the base64 lines started with 2 SPC chars, which is invalid,
316
+ # there will be extra spaces in @value. Since no SPC chars show up in
317
+ # b64 encodings, they can be safely stripped out before unpacking.
318
+ when 'B', 'BASE64' then @value.gsub(' ', '').unpack('m*').first
319
+
320
+ when 'QUOTED-PRINTABLE' then @value.unpack('M*').first
321
+
322
+ else
323
+ raise Vpim::InvalidEncodingError, "unrecognized encoding (#{encoding})"
324
+ end
325
+ end
326
+
327
+ # Is the #name of this Field +name+? Names are case insensitive.
328
+ def name?(name)
329
+ Vpim::Methods.casecmp?(@name, name)
330
+ end
331
+
332
+ # Is the #group of this field +group+? Group names are case insensitive.
333
+ # A +group+ of nil matches if the field has no group.
334
+ def group?(group)
335
+ Vpim::Methods.casecmp?(@group, group)
336
+ end
337
+
338
+ # Is the value of this field of type +kind+? RFC2425 allows the type of
339
+ # a fields value to be encoded in the VALUE parameter. Don't rely on its
340
+ # presence, they aren't required, and usually aren't bothered with. In
341
+ # cases where the kind of value might vary (an iCalendar DTSTART can be
342
+ # either a date or a date-time, for example), you are more likely to see
343
+ # the kind of value specified explicitly.
344
+ #
345
+ # The value types defined by RFC 2425 are:
346
+ # - uri:
347
+ # - text:
348
+ # - date: a list of 1 or more dates
349
+ # - time: a list of 1 or more times
350
+ # - date-time: a list of 1 or more date-times
351
+ # - integer:
352
+ # - boolean:
353
+ # - float:
354
+ def kind?(kind)
355
+ Vpim::Methods.casecmp?(self.kind == kind)
356
+ end
357
+
358
+ # Is one of the values of the TYPE parameter of this field +type+? The
359
+ # type parameter values are case insensitive. False if there is no TYPE
360
+ # parameter.
361
+ #
362
+ # TYPE parameters are used for general categories, such as
363
+ # distinguishing between an email address used at home or at work.
364
+ def type?(type)
365
+ type = type.to_str
366
+
367
+ types = param('TYPE')
368
+
369
+ if types
370
+ types = types.detect { |t| Vpim::Methods.casecmp?(t, type) }
371
+ end
372
+ end
373
+
374
+ # Is this field marked as preferred? A vCard field is preferred if
375
+ # #type?('PREF'). This method is not necessarily meaningful for
376
+ # non-vCard profiles.
377
+ def pref?
378
+ type? 'PREF'
379
+ end
380
+
381
+ # Set whether a field is marked as preferred. See #pref?
382
+ def pref=(ispref)
383
+ if ispref
384
+ pvalue_iadd('TYPE', 'PREF')
385
+ else
386
+ pvalue_idel('TYPE', 'PREF')
387
+ end
388
+ end
389
+
390
+ # Is the value of this field +value+? The check is case insensitive.
391
+ # FIXME - it shouldn't be insensitive, make a #casevalue? method.
392
+ def value?(value)
393
+ Vpim::Methods.casecmp?(@value, value.to_str)
394
+ end
395
+
396
+ # The value of the ENCODING parameter, if present, or nil if not
397
+ # present.
398
+ def encoding
399
+ e = param('ENCODING')
400
+
401
+ if e
402
+ if e.length > 1
403
+ raise Vpim::InvalidEncodingError, "multi-valued param 'ENCODING' (#{e})"
404
+ end
405
+ e = e.first.upcase
406
+ end
407
+ e
408
+ end
409
+
410
+ # The type of the value, as specified by the VALUE parameter, nil if
411
+ # unspecified.
412
+ def kind
413
+ v = param('VALUE')
414
+ if v
415
+ if v.size > 1
416
+ raise InvalidEncodingError, "multi-valued param 'VALUE' (#{values})"
417
+ end
418
+ v = v.first.downcase
419
+ end
420
+ v
421
+ end
422
+
423
+ # The value as an array of Time objects (all times and dates in
424
+ # RFC2425 are lists, even where it might not make sense, such as a
425
+ # birthday). The time will be UTC if marked as so (with a timezone of
426
+ # "Z"), and in localtime otherwise.
427
+ #
428
+ # TODO - support timezone offsets
429
+ #
430
+ # TODO - if year is before 1970, this won't work... but some people
431
+ # are generating calendars saying Canada Day started in 1753!
432
+ # That's just wrong! So, what to do? I add a message
433
+ # saying what the year is that breaks, so they at least know that
434
+ # its ridiculous! I think I need my own DateTime variant.
435
+ def to_time
436
+ begin
437
+ Vpim.decode_date_time_list(value).collect do |d|
438
+ # We get [ year, month, day, hour, min, sec, usec, tz ]
439
+ begin
440
+ if(d.pop == "Z")
441
+ Time.gm(*d)
442
+ else
443
+ Time.local(*d)
444
+ end
445
+ rescue ArgumentError => e
446
+ raise Vpim::InvalidEncodingError, "Time.gm(#{d.join(', ')}) failed with #{e.message}"
447
+ end
448
+ end
449
+ rescue Vpim::InvalidEncodingError
450
+ Vpim.decode_date_list(value).collect do |d|
451
+ # We get [ year, month, day ]
452
+ begin
453
+ Time.gm(*d)
454
+ rescue ArgumentError => e
455
+ raise Vpim::InvalidEncodingError, "Time.gm(#{d.join(', ')}) failed with #{e.message}"
456
+ end
457
+ end
458
+ end
459
+ end
460
+
461
+ # The value as an array of Date objects (all times and dates in
462
+ # RFC2425 are lists, even where it might not make sense, such as a
463
+ # birthday).
464
+ #
465
+ # The field value may be a list of either DATE or DATE-TIME values,
466
+ # decoding is tried first as a DATE-TIME, then as a DATE, if neither
467
+ # works an InvalidEncodingError will be raised.
468
+ def to_date
469
+ begin
470
+ Vpim.decode_date_time_list(value).collect do |d|
471
+ # We get [ year, month, day, hour, min, sec, usec, tz ]
472
+ Date.new(d[0], d[1], d[2])
473
+ end
474
+ rescue Vpim::InvalidEncodingError
475
+ Vpim.decode_date_list(value).collect do |d|
476
+ # We get [ year, month, day ]
477
+ Date.new(*d)
478
+ end
479
+ end
480
+ end
481
+
482
+ # The value as text. Text can have escaped newlines, commas, and escape
483
+ # characters, this method will strip them, if present.
484
+ #
485
+ # In theory, #value could also do this, but it would need to know that
486
+ # the value is of type 'TEXT', and often for text values the 'VALUE'
487
+ # parameter is not present, so knowledge of the expected type of the
488
+ # field is required from the decoder.
489
+ def to_text
490
+ Vpim.decode_text(value)
491
+ end
492
+
493
+ # The undecoded value, see +value+.
494
+ def value_raw
495
+ @value
496
+ end
497
+
498
+ # TODO def pretty_print() ...
499
+
500
+ # Set the group of this field to +group+.
501
+ def group=(group)
502
+ mutate(group, @name, @params, @value)
503
+ group
504
+ end
505
+
506
+ # Set the value of this field to +value+. Valid values are as in
507
+ # Field.create().
508
+ def value=(value)
509
+ mutate(@group, @name, @params, value)
510
+ value
511
+ end
512
+
513
+ # Convert +value+ to text, then assign.
514
+ #
515
+ # TODO - unimplemented
516
+ def text=(text)
517
+ end
518
+
519
+ # Set a the param +pname+'s value to +pvalue+, replacing any value it
520
+ # currently has. See Field.create() for a description of +pvalue+.
521
+ #
522
+ # Example:
523
+ # if field['TYPE']
524
+ # field['TYPE'] << 'HOME'
525
+ # else
526
+ # field['TYPE'] = [ 'HOME' ]
527
+ # end
528
+ #
529
+ # TODO - this could be an alias to #pvalue_set
530
+ def []=(pname,pvalue)
531
+ unless pvalue.respond_to?(:to_ary)
532
+ pvalue = [ pvalue ]
533
+ end
534
+
535
+ h = @params.dup
536
+
537
+ h[pname.upcase] = pvalue
538
+
539
+ mutate(@group, @name, h, @value)
540
+ pvalue
541
+ end
542
+
543
+ # Add +pvalue+ to the param +pname+'s value. The values are treated as a
544
+ # set so duplicate values won't occur, and String values are case
545
+ # insensitive. See Field.create() for a description of +pvalue+.
546
+ def pvalue_iadd(pname, pvalue)
547
+ pname = pname.upcase
548
+
549
+ # Get a uniq set, where strings are compared case-insensitively.
550
+ values = [ pvalue, @params[pname] ].flatten.compact
551
+ values = values.collect do |v|
552
+ if v.respond_to? :to_str
553
+ v = v.to_str.upcase
554
+ end
555
+ v
556
+ end
557
+ values.uniq!
558
+
559
+ h = @params.dup
560
+
561
+ h[pname] = values
562
+
563
+ mutate(@group, @name, h, @value)
564
+ values
565
+ end
566
+
567
+ # Delete +pvalue+ from the param +pname+'s value. The values are treated
568
+ # as a set so duplicate values won't occur, and String values are case
569
+ # insensitive. +pvalue+ must be a single String or Symbol.
570
+ def pvalue_idel(pname, pvalue)
571
+ pname = pname.upcase
572
+ if pvalue.respond_to? :to_str
573
+ pvalue = pvalue.to_str.downcase
574
+ end
575
+
576
+ # Get a uniq set, where strings are compared case-insensitively.
577
+ values = [ nil, @params[pname] ].flatten.compact
578
+ values = values.collect do |v|
579
+ if v.respond_to? :to_str
580
+ v = v.to_str.downcase
581
+ end
582
+ v
583
+ end
584
+ values.uniq!
585
+ values.delete pvalue
586
+
587
+ h = @params.dup
588
+
589
+ h[pname] = values
590
+
591
+ mutate(@group, @name, h, @value)
592
+ values
593
+ end
594
+
595
+ # FIXME - should change this so it doesn't assign to @line here, so @line
596
+ # is used to preserve original encoding. That way, #encode can only wrap
597
+ # new fields, not old fields.
598
+ def mutate(g, n, p, v) #:nodoc:
599
+ line = Field.encode0(g, n, p, v)
600
+
601
+ begin
602
+ @group, @name, @params, @value = Field.decode0(line)
603
+ @line = line
604
+ rescue Vpim::InvalidEncodingError => e
605
+ raise ArgumentError, e.to_s
606
+ end
607
+ self
608
+ end
609
+
610
+ private :mutate
611
+ end
612
+ end
613
+ end
614
+