virginity 0.3.31

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 (42) hide show
  1. checksums.yaml +7 -0
  2. data/lib/virginity.rb +6 -0
  3. data/lib/virginity/api_extensions.rb +87 -0
  4. data/lib/virginity/api_extensions/fields_to_json.rb +82 -0
  5. data/lib/virginity/api_extensions/fields_to_xml.rb +151 -0
  6. data/lib/virginity/bnf.rb +84 -0
  7. data/lib/virginity/dir_info.rb +93 -0
  8. data/lib/virginity/dir_info/content_line.rb +146 -0
  9. data/lib/virginity/dir_info/line_folding.rb +60 -0
  10. data/lib/virginity/dir_info/param.rb +208 -0
  11. data/lib/virginity/dir_info/query.rb +144 -0
  12. data/lib/virginity/encoding_decoding.rb +177 -0
  13. data/lib/virginity/encodings.rb +36 -0
  14. data/lib/virginity/fixes.rb +230 -0
  15. data/lib/virginity/vcard.rb +244 -0
  16. data/lib/virginity/vcard/base_field.rb +126 -0
  17. data/lib/virginity/vcard/categories.rb +57 -0
  18. data/lib/virginity/vcard/cleaning.rb +364 -0
  19. data/lib/virginity/vcard/field.rb +22 -0
  20. data/lib/virginity/vcard/field/params.rb +93 -0
  21. data/lib/virginity/vcard/field_values.rb +10 -0
  22. data/lib/virginity/vcard/field_values/binary.rb +22 -0
  23. data/lib/virginity/vcard/field_values/boolean.rb +14 -0
  24. data/lib/virginity/vcard/field_values/case_insensitive_value.rb +13 -0
  25. data/lib/virginity/vcard/field_values/date.rb +16 -0
  26. data/lib/virginity/vcard/field_values/integer.rb +15 -0
  27. data/lib/virginity/vcard/field_values/optional_structured_text.rb +35 -0
  28. data/lib/virginity/vcard/field_values/separated_text.rb +59 -0
  29. data/lib/virginity/vcard/field_values/structured_text.rb +71 -0
  30. data/lib/virginity/vcard/field_values/text.rb +23 -0
  31. data/lib/virginity/vcard/field_values/uri.rb +15 -0
  32. data/lib/virginity/vcard/fields.rb +284 -0
  33. data/lib/virginity/vcard/fields_osx.rb +95 -0
  34. data/lib/virginity/vcard/fields_soocial.rb +45 -0
  35. data/lib/virginity/vcard/name_handler.rb +151 -0
  36. data/lib/virginity/vcard/patching.rb +262 -0
  37. data/lib/virginity/vcard21.rb +2 -0
  38. data/lib/virginity/vcard21/base.rb +30 -0
  39. data/lib/virginity/vcard21/parser.rb +359 -0
  40. data/lib/virginity/vcard21/reader.rb +103 -0
  41. data/lib/virginity/vcard21/writer.rb +139 -0
  42. metadata +111 -0
@@ -0,0 +1,22 @@
1
+ require 'virginity/vcard/base_field'
2
+
3
+ module Virginity
4
+
5
+ # Basic field, if we don't know anything about it, we assume it can at least handle text encoding
6
+ class Field < BaseField
7
+ include FieldValues::Text
8
+
9
+ field_register.default = self
10
+ end
11
+
12
+
13
+ # monkey patch ContentLine to make a #to_field method
14
+ class ContentLine
15
+ # convert to a vcard-field (see Field)
16
+ def to_field
17
+ Field.parse(self)
18
+ end
19
+ end
20
+
21
+
22
+ end
@@ -0,0 +1,93 @@
1
+ module Virginity
2
+
3
+ module Params
4
+ module Type
5
+
6
+
7
+ class TypeArray < SerializingArray
8
+ def initialize(field)
9
+ @field = field
10
+ super(@field.params("TYPE").map { |p| p.value }.uniq)
11
+ end
12
+
13
+ # def reload!
14
+ # @array = Array.new(@field.params("TYPE").map { |p| p.value }.uniq)
15
+ # self
16
+ # end
17
+
18
+ def rewrite!
19
+ @field.params("TYPE").each {|t| @field.params.delete t }
20
+ @array.each do |type|
21
+ @field.params << Param.new('TYPE', type)
22
+ end
23
+ end
24
+
25
+ # Locations are a subset of all the TYPE-params
26
+ LOCATIONS = { "CELL" => "Mobile", "HOME" => "Home", "OTHER" => "Other", "WORK" => "Work" }
27
+ def locations
28
+ @array.select { |t| LOCATIONS.keys.include?(t)}
29
+ end
30
+
31
+ def locations=(locs)
32
+ locations.each {|l| delete(l) }
33
+ locs.each { |l| self << l }
34
+ end
35
+ end
36
+
37
+
38
+ def types
39
+ TypeArray.new(self)
40
+ end
41
+
42
+ def types=(array)
43
+ types.replace(array)
44
+ end
45
+
46
+ def type=(str)
47
+ self.types = str.split(/ /)
48
+ end
49
+
50
+ def add_type(thing)
51
+ t = types
52
+ t << thing unless t.include? thing
53
+ end
54
+
55
+ def remove_type(thing)
56
+ types.delete(thing)
57
+ end
58
+
59
+ # =============================
60
+ # Preferred
61
+ def preferred?
62
+ types.include? 'PREF'
63
+ end
64
+
65
+ def preferred=(val)
66
+ if val
67
+ add_type 'PREF'
68
+ else
69
+ remove_type 'PREF'
70
+ end
71
+ end
72
+
73
+ # =============================
74
+ # Location handling:
75
+ def locations
76
+ types.locations
77
+ end
78
+
79
+ def locations=(array)
80
+ types.locations = array
81
+ end
82
+
83
+ def location
84
+ locations.join(" ")
85
+ end
86
+
87
+ def location=(str)
88
+ self.locations = str.split(/ /)
89
+ end
90
+ end
91
+
92
+ end
93
+ end
@@ -0,0 +1,10 @@
1
+ require 'virginity/vcard/field_values/binary.rb'
2
+ require 'virginity/vcard/field_values/boolean.rb'
3
+ require 'virginity/vcard/field_values/case_insensitive_value.rb'
4
+ require 'virginity/vcard/field_values/date.rb'
5
+ require 'virginity/vcard/field_values/integer.rb'
6
+ require 'virginity/vcard/field_values/separated_text.rb'
7
+ require 'virginity/vcard/field_values/structured_text.rb'
8
+ require 'virginity/vcard/field_values/optional_structured_text.rb'
9
+ require 'virginity/vcard/field_values/text.rb'
10
+ require 'virginity/vcard/field_values/uri.rb'
@@ -0,0 +1,22 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ module Binary
5
+ def binary
6
+ Base64.decode64(@value)
7
+ end
8
+
9
+ def binary=(s)
10
+ @params.delete_if { |p| p.key == "ENCODING" }
11
+ @params << Param.new("ENCODING", "b")
12
+ b64 = Base64.encode64(s)
13
+ b64.delete!("\n") # can return nil... bah, but probably faster than #delete without an exclamation mark
14
+ @value = b64
15
+ end
16
+
17
+ def sha1
18
+ Digest::SHA1.hexdigest(@value)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,14 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ module Boolean
5
+ def boolean
6
+ (@value.downcase == "true")
7
+ end
8
+
9
+ def boolean=(b)
10
+ @value = b ? "true" : "false"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ module CaseInsensitiveValue
5
+ def ==(other)
6
+ group == other.group &&
7
+ has_name?(other.name) &&
8
+ params == other.params &&
9
+ (raw_value.casecmp(other.raw_value) == 0)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ require 'date'
2
+
3
+ module Virginity
4
+ module FieldValues
5
+
6
+ module DateValue
7
+ def date
8
+ Date.parse(text)
9
+ end
10
+
11
+ def date=(d)
12
+ self.text = d.to_s
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ module Integer
5
+ def integer
6
+ @value.to_i
7
+ end
8
+
9
+ def integer=(i)
10
+ @value = @integer.to_s
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,35 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ class OptionalStructuredText < StructuredText
5
+ def self.define(components)
6
+ m = super
7
+
8
+ m.module_eval <<-RUBY, __FILE__, __LINE__+1
9
+ def reencode!(options = {})
10
+ v = values
11
+
12
+ v.pop while v.last.empty?
13
+
14
+ @value = EncodingDecoding::encode_structured_text(v)
15
+ end
16
+ RUBY
17
+
18
+ components.each_with_index do |component, idx|
19
+ m.module_eval <<-RUBY, __FILE__, __LINE__+1
20
+ def #{component}=(new_value)
21
+ structure = values
22
+ structure[#{idx}] = new_value.to_s
23
+
24
+ structure.pop while structure.size > 0 && (structure.last.nil? || structure.last.empty?)
25
+
26
+ @value = EncodingDecoding::encode_structured_text(structure)
27
+ end
28
+ RUBY
29
+ end
30
+
31
+ m
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,59 @@
1
+ require 'reactive_array'
2
+ require 'digest/sha1'
3
+
4
+ module Virginity
5
+ module FieldValues
6
+
7
+ module SeparatedText
8
+
9
+
10
+ class TextList < SerializingArray
11
+ def initialize(field)
12
+ @field = field # a reference to the original Field
13
+ super(EncodingDecoding::decode_text_list(@field.raw_value))
14
+ save_sha1!
15
+ end
16
+
17
+ def sha1
18
+ Digest::SHA1.hexdigest(@field.raw_value)
19
+ end
20
+
21
+ def save_sha1!
22
+ @sha1 = sha1
23
+ end
24
+
25
+ def needs_refresh?
26
+ @sha1 != sha1
27
+ end
28
+
29
+ def rewrite!
30
+ @array.delete_if {|v| v.empty? }
31
+ @field.raw_value = EncodingDecoding::encode_text_list(@array)
32
+ save_sha1!
33
+ end
34
+ end
35
+
36
+
37
+ def values
38
+ if (@textlist.needs_refresh? rescue true)
39
+ @textlist = TextList.new(self)
40
+ else
41
+ @textlist
42
+ end
43
+ end
44
+
45
+ def values=(a)
46
+ values.replace(a)
47
+ end
48
+
49
+ def reencode!
50
+ values.rewrite!
51
+ end
52
+
53
+ def subset_of?(other)
54
+ values.all? { |v| other.values.include? v }
55
+ end
56
+ end
57
+
58
+ end
59
+ end
@@ -0,0 +1,71 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ class StructuredText
5
+ def self.define(components)
6
+ m = Module.new
7
+ m.const_set("COMPONENTS", components)
8
+ m.module_eval <<-RUBY
9
+ def components
10
+ COMPONENTS
11
+ end
12
+
13
+ def values
14
+ EncodingDecoding::decode_structured_text(@value, COMPONENTS.size)
15
+ end
16
+
17
+ def empty?
18
+ values.all? {|v| v.empty? }
19
+ end
20
+
21
+ def reencode!(options={})
22
+ v = values
23
+ unless options[:variable_number_of_fields]
24
+ v.pop while v.size > components.size
25
+ v.push(nil) while v.size < components.size
26
+ else
27
+ v.pop while v.last.empty?
28
+ end
29
+ @value = EncodingDecoding::encode_structured_text(v)
30
+ end
31
+
32
+ def [](component)
33
+ raise "which component? \#\{component\}?? I only know \#\{components.inspect\}" unless components.include? component.to_sym
34
+ send("\#\{component\}".to_sym)
35
+ end
36
+
37
+ def []=(component, new_value)
38
+ raise "which component? \#\{component\}?? I only know \#\{components.inspect\}" unless components.include? component.to_sym
39
+ send("\#\{component\}=", new_value)
40
+ end
41
+ RUBY
42
+
43
+ components.each_with_index do |component, idx|
44
+ m.module_eval <<-RUBY
45
+ def #{component}
46
+ values[#{idx}]
47
+ end
48
+
49
+ def #{component}=(new_value)
50
+ structure = values
51
+ structure[#{idx}] = new_value.to_s
52
+ @value = EncodingDecoding::encode_structured_text(structure)
53
+ end
54
+
55
+ def subset_of?(other_field)
56
+ components.all? do |component|
57
+ send(component).empty? or send(component) == other_field.send(component)
58
+ end
59
+ end
60
+
61
+ def superset_of?(other_field)
62
+ other_field.subset_of?(self)
63
+ end
64
+ RUBY
65
+ end
66
+ m
67
+ end
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,23 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ module Text
5
+ def text
6
+ EncodingDecoding::decode_text(@value)
7
+ end
8
+
9
+ def text=(s)
10
+ @params.delete_if { |p| p.key == "ENCODING" }
11
+ @value = EncodingDecoding::encode_text(s)
12
+ end
13
+
14
+ def reencode!
15
+ self.text = text
16
+ end
17
+
18
+ def value_to_xml
19
+ xml_element("text", text.strip)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ module Virginity
2
+ module FieldValues
3
+
4
+ # needs module Text
5
+ module Uri
6
+ def uri
7
+ URI::parse(text)
8
+ end
9
+
10
+ def uri=(new_uri)
11
+ self.text = new_uri.to_s
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,284 @@
1
+ require "virginity/vcard/field"
2
+ require "virginity/vcard/field/params"
3
+ require "base64"
4
+
5
+ module Virginity
6
+
7
+ # BEGIN or END
8
+ #
9
+ # We don't do much with them
10
+ class BeginEnd < BaseField
11
+ include FieldValues::CaseInsensitiveValue
12
+ # value MUST be "VCARD"
13
+ register_for :BEGIN, :END
14
+ end
15
+
16
+
17
+ class Profile < BaseField
18
+ register_for :PROFILE
19
+ end
20
+
21
+
22
+ # Text fields, see FieldValues::Text for safe encoding/decoding methods
23
+ class TextField < BaseField
24
+ include FieldValues::Text
25
+ register_for :CLASS, :FN, :LABEL, :MAILER, :NOTE, :PRODID, :ROLE, 'SORT-STRING', :TITLE, :UID, :VERSION, :'X-PHONETIC-LAST-NAME', :'X-PHONETIC-FIRST-NAME'
26
+ end
27
+
28
+
29
+ # A BDAY in a vCard can be a free form text-value or a date. This class provides methods for both cases.
30
+ class Birthday < BaseField
31
+ include FieldValues::Text
32
+ include FieldValues::DateValue
33
+ register_for :BDAY
34
+ end
35
+
36
+
37
+ class Anniversary < BaseField
38
+ include FieldValues::Text
39
+ include FieldValues::DateValue
40
+ register_for "X-ANNIVERSARY"
41
+ end
42
+
43
+
44
+ # Instant messaging fields are defined in rfc 2427. They have a scheme and an address. If an IMPP is not 'clean' one can still get/set the value by using the methods from FieldValues::Text and FieldValues::Uri
45
+ class Impp < BaseField
46
+ include FieldValues::Text
47
+ include FieldValues::Uri
48
+ include Params::Type
49
+ register_for :IMPP
50
+ # include LocationHandling
51
+ # include PurposeHandling
52
+ # include PreferenceHandling
53
+ # PURPOSE_SETS = { "personal" => "Personal", "business" => "Business" }
54
+ # HUMAN_DESCRIPTION = "instant messaging"
55
+
56
+ # def initialize(content_line=ContentLine.new("IMPP:"))
57
+ # super
58
+ # @cline.name = "IMPP"
59
+ # end
60
+
61
+ def scheme
62
+ # every piece of text before the first colon, if there is a colon present
63
+ text.match(/^(.*?):/) # Note the use of "*?" for non greedy matching of the colon!
64
+ $1 || ""
65
+ end
66
+
67
+ def address
68
+ # everything after the first colon if that colon is present, otherwise the whole text
69
+ text.match(/^.*?:(.*)$|^(.*)$/)
70
+ $1 || $2
71
+ end
72
+
73
+ def scheme=(s)
74
+ self.text = "#{s}:#{address}"
75
+ end
76
+
77
+ def address=(s)
78
+ self.text = "#{self.scheme}:#{s}"
79
+ end
80
+
81
+ def raw_value
82
+ scheme.empty? && address.empty? ? "" : @value
83
+ end
84
+ end
85
+
86
+
87
+ # OS X AddressBook does not use the IMPP fields. Instead Apple chose to use their own proprietary format. This format is handles by CustomImField
88
+ class CustomImField < BaseField
89
+ include FieldValues::Text
90
+ PROTOCOL_TRANSLATION_TABLE = {
91
+ :aim => "X-AIM",
92
+ :msn => "X-MSN",
93
+ :ymsgr => "X-YAHOO",
94
+ :skype => "X-SKYPE",
95
+ :qq => "X-QQ",
96
+ :gtalk => "X-GOOGLE TALK",
97
+ :icq => "X-ICQ",
98
+ :xmpp => "X-JABBER",
99
+ }
100
+ PROTOCOL_TRANSLATION_TABLE.values.each { |protocol| register_for protocol }
101
+ PROTOCOL_TRANSLATION_INVERSE_TABLE = Hash[PROTOCOL_TRANSLATION_TABLE.map { |k, v| [v, k] }]
102
+
103
+ # convert a standard IMPP field to a Custom IM field. Only schemes defined in PROTOCOL_TRANSLATION_TABLE are supported.
104
+ def self.from_impp(impp)
105
+ nm = PROTOCOL_TRANSLATION_TABLE[impp.scheme.to_sym]
106
+ raise "unknown scheme #{impp.scheme} for #{impp.text.inspect}" if nm.nil?
107
+ x = Field.parse("#{nm}:")
108
+ x.params = Param::deep_copy(impp.params)
109
+ x.text = impp.address
110
+ x
111
+ end
112
+
113
+ def protocol
114
+ PROTOCOL_TRANSLATION_INVERSE_TABLE[name]
115
+ end
116
+
117
+ # convert to a standard IMPP field
118
+ def to_impp
119
+ impp = Field.parse("IMPP:")
120
+ impp.params = Param::deep_copy(@params)
121
+ impp.text = "#{protocol}:#{text}"
122
+ impp
123
+ end
124
+ end
125
+
126
+
127
+ # handle ORG fields.
128
+ #
129
+ # An Org has an orgname, a unit1 and a unit2. We stick to this simple
130
+ # definition for now since it is widely used. The vCard specs seem to
131
+ # specify an unlimited amount of units
132
+ class Org < BaseField
133
+ register_for :ORG
134
+ include FieldValues::StructuredText.define([:orgname, :unit1, :unit2])
135
+
136
+ def shortened
137
+ [orgname, unit1, unit2].join(" ").strip
138
+ end
139
+
140
+ # def ==(other)
141
+ # super ||
142
+ # has_name?(other.name) &&
143
+ # values == other.try(:values) && self.class === other && self.group == other.group
144
+ # end
145
+ end
146
+
147
+
148
+ # SeparatedField handles an array of text values
149
+ class SeparatedField < BaseField
150
+ include FieldValues::SeparatedText
151
+ register_for :CATEGORIES, :NICKNAME
152
+
153
+ def unpacked
154
+ values.map do |text|
155
+ self.class.new(name, EncodingDecoding::encode_text_list([text]), params, group)
156
+ end
157
+ end
158
+ end
159
+
160
+
161
+ # telephone number
162
+ #
163
+ # provides the easy getter/setter #number and generators for random numbers
164
+ class Tel < BaseField
165
+ include FieldValues::Text
166
+ include Params::Type
167
+ # include Params::Type::Preference
168
+ register_for :TEL
169
+ COMPARISON_REGEX = /[^\+\*\#\/\,\w]/u # anything that is NOT a plus, a star, a hash, a slash, a comma, or 0-9/a-z/A-Z is insignificant
170
+
171
+ alias_method :number, :text
172
+ alias_method :number=, :text=
173
+
174
+ def self.random_number(length = 10)
175
+ # (1..length).map { rand(10).to_s }.join
176
+ # only 555-0100 through 555-0199 are now specifically reserved for fictional use
177
+ random_part = 100 + (rand(99) + 1)
178
+ "555-0#{random_part}"
179
+ end
180
+
181
+ def self.at_random
182
+ new("TEL", random_number)
183
+ end
184
+
185
+ def significant_chars
186
+ # it's a phone number (but people could store alphanumeric stuff here) so I opt to remove all dashes and spaces
187
+ # I leave the plus since we don't know with what prefix to replace it with, see: http://en.wikipedia.org/wiki/List_of_international_call_prefixes
188
+ number.gsub(COMPARISON_REGEX, "")
189
+ end
190
+ alias_method :normalized_number, :significant_chars
191
+
192
+ end
193
+
194
+
195
+ class Email < BaseField
196
+ include FieldValues::Text
197
+ include Params::Type
198
+ register_for :EMAIL
199
+ alias_method :address, :text
200
+ alias_method :address=, :text=
201
+ end
202
+
203
+
204
+ class Adr < BaseField
205
+ include FieldValues::StructuredText.define([:pobox, :extended, :street, :locality, :region, :postal_code, :country])
206
+ include Params::Type
207
+ register_for :ADR
208
+ NAME = "ADR"
209
+ end
210
+
211
+
212
+ class Url < BaseField
213
+ include FieldValues::Text
214
+ include FieldValues::Uri
215
+ # URLs officialy don't have TYPE params,
216
+ # but everyone and their dog thinks they do.
217
+ # So we decided to support them too.
218
+ # Conclusion: no-one reads the RFC
219
+ include Params::Type
220
+ register_for :URL
221
+ end
222
+
223
+
224
+ class Name < BaseField
225
+ PARTS = %w(family given additional prefix suffix)
226
+ include FieldValues::StructuredText.define(PARTS)
227
+ register_for :N
228
+ end
229
+
230
+
231
+ class Photo < BaseField
232
+ include FieldValues::Text
233
+ include FieldValues::Binary
234
+ include FieldValues::Uri
235
+ register_for :LOGO, :PHOTO
236
+
237
+ def is_binary?
238
+ @params.detect {|p| p.key == "ENCODING" and p.value =~ /B/i }
239
+ end
240
+
241
+ def is_url?
242
+ !is_binary?
243
+ end
244
+ end
245
+
246
+
247
+ class Sound < BaseField
248
+ include FieldValues::Binary
249
+ include FieldValues::Text
250
+ include FieldValues::Uri
251
+ register_for :SOUND
252
+ end
253
+
254
+
255
+ class Key < BaseField
256
+ include FieldValues::Binary
257
+ include FieldValues::Text
258
+ register_for :KEY
259
+ end
260
+
261
+ class Gender < BaseField
262
+ register_for :GENDER
263
+ PARTS = %w(sex identity)
264
+ include FieldValues::OptionalStructuredText.define(PARTS)
265
+
266
+ {male: 'M', female: 'F', other: 'O', none: 'N', unknown: 'U'}.each do |name, letter|
267
+ class_eval <<-RUBY
268
+ def #{name}?
269
+ sex == "#{letter}"
270
+ end
271
+
272
+ def #{name}=(v)
273
+ raise "Only true is acceptable" unless v
274
+ self.sex = "#{letter}"
275
+ end
276
+ RUBY
277
+ end
278
+
279
+ def neither?
280
+ sex !~ /^[MFNOU]$/
281
+ end
282
+ end
283
+
284
+ end