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.
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