vcard 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/.gitignore +15 -5
  2. data/Gemfile +3 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +17 -0
  5. data/Rakefile +4 -52
  6. data/{LICENSE → VPIM-LICENSE.txt} +2 -2
  7. data/lib/vcard.rb +304 -28
  8. data/lib/vcard/attachment.rb +10 -12
  9. data/lib/vcard/bnf.rb +66 -0
  10. data/lib/vcard/dirinfo.rb +24 -26
  11. data/lib/vcard/enumerator.rb +5 -7
  12. data/lib/vcard/errors.rb +23 -0
  13. data/lib/vcard/field.rb +56 -58
  14. data/lib/vcard/vcard.rb +210 -240
  15. data/lib/vcard/version.rb +3 -0
  16. data/test/field_test.rb +55 -55
  17. data/test/fixtures/bday_decode.vcard +3 -0
  18. data/test/fixtures/bday_decode_2.vcard +6 -0
  19. data/test/fixtures/empty_tel.vcard +3 -0
  20. data/test/fixtures/ex1.vcard +7 -0
  21. data/test/fixtures/ex2.vcard +9 -0
  22. data/test/fixtures/ex3.vcard +30 -0
  23. data/test/fixtures/ex_21.vcard +16 -0
  24. data/test/fixtures/ex_21_case0.vcard +15 -0
  25. data/test/fixtures/ex_apple1.vcard +13 -0
  26. data/test/fixtures/ex_attach.vcard +16 -0
  27. data/test/fixtures/ex_bdays.vcard +8 -0
  28. data/test/fixtures/ex_encode_1.vcard +10 -0
  29. data/test/fixtures/ex_ical_1.vcal +47 -0
  30. data/test/fixtures/gmail.vcard +27 -0
  31. data/test/fixtures/highrise.vcard +41 -0
  32. data/test/fixtures/multiple_occurences_of_type.vcard +17 -0
  33. data/test/fixtures/nickname0.vcard +2 -0
  34. data/test/fixtures/nickname1.vcard +3 -0
  35. data/test/fixtures/nickname2.vcard +3 -0
  36. data/test/fixtures/nickname3.vcard +3 -0
  37. data/test/fixtures/nickname4.vcard +4 -0
  38. data/test/fixtures/nickname5.vcard +5 -0
  39. data/test/fixtures/slash_in_field_name.vcard +3 -0
  40. data/test/fixtures/tst1.vcard +9 -0
  41. data/test/fixtures/url_decode.vcard +4 -0
  42. data/test/test_helper.rb +34 -6
  43. data/test/vcard_test.rb +87 -577
  44. data/vcard.gemspec +19 -0
  45. metadata +88 -43
  46. data/.document +0 -5
  47. data/README.rdoc +0 -7
  48. data/VERSION +0 -1
  49. data/lib/vcard/rfc2425.rb +0 -367
data/.gitignore CHANGED
@@ -1,7 +1,17 @@
1
- *.sw?
2
- .DS_Store
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
3
9
  coverage
4
- rdoc
10
+ doc/
11
+ lib/bundler/man
5
12
  pkg
6
- *.gem
7
- *.gemspec
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
@@ -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.
@@ -0,0 +1,17 @@
1
+ # Vcard
2
+
3
+ Vcard gem extracts Vcard support from Vpim gem.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem "vcard"
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install vcard
data/Rakefile CHANGED
@@ -1,58 +1,10 @@
1
- # encoding: UTF-8
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 << 'lib' << 'test'
24
- test.pattern = 'test/**/*_test.rb'
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
 
@@ -1,34 +1,310 @@
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
- #:main:README
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