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.
- checksums.yaml +7 -0
- data/lib/virginity.rb +6 -0
- data/lib/virginity/api_extensions.rb +87 -0
- data/lib/virginity/api_extensions/fields_to_json.rb +82 -0
- data/lib/virginity/api_extensions/fields_to_xml.rb +151 -0
- data/lib/virginity/bnf.rb +84 -0
- data/lib/virginity/dir_info.rb +93 -0
- data/lib/virginity/dir_info/content_line.rb +146 -0
- data/lib/virginity/dir_info/line_folding.rb +60 -0
- data/lib/virginity/dir_info/param.rb +208 -0
- data/lib/virginity/dir_info/query.rb +144 -0
- data/lib/virginity/encoding_decoding.rb +177 -0
- data/lib/virginity/encodings.rb +36 -0
- data/lib/virginity/fixes.rb +230 -0
- data/lib/virginity/vcard.rb +244 -0
- data/lib/virginity/vcard/base_field.rb +126 -0
- data/lib/virginity/vcard/categories.rb +57 -0
- data/lib/virginity/vcard/cleaning.rb +364 -0
- data/lib/virginity/vcard/field.rb +22 -0
- data/lib/virginity/vcard/field/params.rb +93 -0
- data/lib/virginity/vcard/field_values.rb +10 -0
- data/lib/virginity/vcard/field_values/binary.rb +22 -0
- data/lib/virginity/vcard/field_values/boolean.rb +14 -0
- data/lib/virginity/vcard/field_values/case_insensitive_value.rb +13 -0
- data/lib/virginity/vcard/field_values/date.rb +16 -0
- data/lib/virginity/vcard/field_values/integer.rb +15 -0
- data/lib/virginity/vcard/field_values/optional_structured_text.rb +35 -0
- data/lib/virginity/vcard/field_values/separated_text.rb +59 -0
- data/lib/virginity/vcard/field_values/structured_text.rb +71 -0
- data/lib/virginity/vcard/field_values/text.rb +23 -0
- data/lib/virginity/vcard/field_values/uri.rb +15 -0
- data/lib/virginity/vcard/fields.rb +284 -0
- data/lib/virginity/vcard/fields_osx.rb +95 -0
- data/lib/virginity/vcard/fields_soocial.rb +45 -0
- data/lib/virginity/vcard/name_handler.rb +151 -0
- data/lib/virginity/vcard/patching.rb +262 -0
- data/lib/virginity/vcard21.rb +2 -0
- data/lib/virginity/vcard21/base.rb +30 -0
- data/lib/virginity/vcard21/parser.rb +359 -0
- data/lib/virginity/vcard21/reader.rb +103 -0
- data/lib/virginity/vcard21/writer.rb +139 -0
- metadata +111 -0
@@ -0,0 +1,126 @@
|
|
1
|
+
require "virginity/dir_info/content_line"
|
2
|
+
require "virginity/vcard/cleaning"
|
3
|
+
require "virginity/vcard21/writer"
|
4
|
+
require "virginity/vcard/field_values"
|
5
|
+
|
6
|
+
module Virginity
|
7
|
+
|
8
|
+
# As a Vcard is a special type of DirectoryInformation, so is the Field a special type of ContentLine.
|
9
|
+
# A Field in Virginity is more content aware than a ContentLine. It knows what sort of content to expect for BDAY or ADR (respectively date or text, and a structured text value) and it provides the according methods that handle the differences in encoding.
|
10
|
+
class BaseField < ContentLine
|
11
|
+
include Vcard21::Writer
|
12
|
+
include FieldCleaning
|
13
|
+
|
14
|
+
def self.merger(left, right)
|
15
|
+
# ContentLine.merger returns a ContentLine, let's convert it to a field again.
|
16
|
+
Field.parse(super)
|
17
|
+
end
|
18
|
+
|
19
|
+
# is this field a preferred field?
|
20
|
+
TYPE = "TYPE"
|
21
|
+
PREF = /^pref$/i
|
22
|
+
def pref?
|
23
|
+
params(TYPE).any? { |p| p.value =~ PREF }
|
24
|
+
end
|
25
|
+
|
26
|
+
BEGIN_REGEX = /^BEGIN$/i
|
27
|
+
END_REGEX = /^END$/i
|
28
|
+
|
29
|
+
# Fields can be sorted
|
30
|
+
def <=>(other)
|
31
|
+
# BEGIN and END are special. and Virginity does not support nested vcards
|
32
|
+
return -1 if name =~ BEGIN_REGEX or other.name =~ END_REGEX
|
33
|
+
return 1 if name =~ END_REGEX or other.name =~ BEGIN_REGEX
|
34
|
+
unless (diff = (name <=> other.name)) == 0
|
35
|
+
diff
|
36
|
+
else
|
37
|
+
unless (diff = (pref? ? 0 : 1) <=> (other.pref? ? 0 : 1)) == 0
|
38
|
+
diff
|
39
|
+
else
|
40
|
+
to_s <=> other.to_s
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# fields ARE content_lines but most often one should not deal with the value. In virtually all cases value contains some encoded text that is only useful when decoded as text or a text list.
|
46
|
+
def value
|
47
|
+
$stderr.puts "WARNING, you probably don't want to read value, if you do, please use #raw_value. Called from: #{caller.first}"
|
48
|
+
raw_value
|
49
|
+
end
|
50
|
+
|
51
|
+
alias_method :raw_value=, :value=
|
52
|
+
def value=(new_value)
|
53
|
+
$stderr.puts "WARNING, you probably don't want to write value, if you do, please use #raw_value=. Called from: #{caller.first}"
|
54
|
+
raw_value=(new_value)
|
55
|
+
end
|
56
|
+
|
57
|
+
def ==(other)
|
58
|
+
group == other.group &&
|
59
|
+
has_name?(other.name) &&
|
60
|
+
params == other.params &&
|
61
|
+
raw_value == other.raw_value
|
62
|
+
end
|
63
|
+
|
64
|
+
|
65
|
+
# a Hash to containing name => class
|
66
|
+
@@field_register = Hash.new(self)
|
67
|
+
|
68
|
+
def self.field_register
|
69
|
+
@@field_register
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.named(name)
|
73
|
+
if registered? name
|
74
|
+
self[name].new(name)
|
75
|
+
else
|
76
|
+
new(name)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# redefine ContentLine#parse to gain a few nanoseconds and initialize the correct Field using the register
|
81
|
+
def self.parse(line)
|
82
|
+
if line.is_a? ContentLine
|
83
|
+
self[line.name].new(line.name, line.raw_value, line.params, line.group)
|
84
|
+
else
|
85
|
+
group, name, params, value = line_parts(line.to_s)
|
86
|
+
self[name].new(name, value, params, group, :no_deep_copy => true)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class << self
|
91
|
+
alias_method :from_line, :parse
|
92
|
+
end
|
93
|
+
|
94
|
+
# register a new field name with the class that should be used to represent it
|
95
|
+
def self.register_field(name, field_class)
|
96
|
+
raise "#{name} is already registered" if registered?(name)
|
97
|
+
@@field_register[name.to_s.upcase] = field_class
|
98
|
+
end
|
99
|
+
|
100
|
+
# when called from a Field-descendant, this method registers that class as the one handling fields with a name in names
|
101
|
+
def self.register_for(*names)
|
102
|
+
names.each { |name| register_field(name, self) }
|
103
|
+
end
|
104
|
+
|
105
|
+
# is name registered?
|
106
|
+
def self.registered?(name)
|
107
|
+
@@field_register.keys.include?(name.to_s.upcase)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.unregister(name)
|
111
|
+
@@field_register[name.to_s.upcase] = nil
|
112
|
+
end
|
113
|
+
|
114
|
+
# TODO: figure out if we really need upcase here
|
115
|
+
def self.[](name)
|
116
|
+
@@field_register[name.to_s.upcase]
|
117
|
+
end
|
118
|
+
|
119
|
+
# # a hash mapping field-names/types to Field-classes
|
120
|
+
def self.types
|
121
|
+
@@field_register.dup
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'reactive_array'
|
2
|
+
require 'virginity/vcard/fields'
|
3
|
+
|
4
|
+
module Virginity
|
5
|
+
|
6
|
+
# methods to handle ALL categories fields in a Vcard. These work around the
|
7
|
+
# difficulties of working with multiple CATEGORIES-lines in a vCard by not
|
8
|
+
# trying to preserve any ordering or grouping.
|
9
|
+
module VcardCategories
|
10
|
+
|
11
|
+
def category_values
|
12
|
+
categories.map {|cat| cat.values.to_a }.flatten.uniq.sort
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_category(c)
|
16
|
+
@tags = nil
|
17
|
+
self.push SeparatedField.new("CATEGORIES", EncodingDecoding::encode_text(c))
|
18
|
+
end
|
19
|
+
|
20
|
+
def remove_category(c)
|
21
|
+
@tags = nil
|
22
|
+
categories.each { |cat| cat.values.delete(c) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def in_category?(c)
|
26
|
+
categories.any? { |cat| cat.values.include?(c) }
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
class TagArray < SerializingArray
|
31
|
+
def initialize(vcard)
|
32
|
+
@vcard = vcard
|
33
|
+
super(@vcard.category_values)
|
34
|
+
end
|
35
|
+
|
36
|
+
def rewrite!
|
37
|
+
@vcard.categories.each {|cat| @vcard.delete_field cat }
|
38
|
+
@array.each do |tag|
|
39
|
+
@vcard.add_category tag
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def tags
|
46
|
+
@tags ||= TagArray.new(self)
|
47
|
+
end
|
48
|
+
|
49
|
+
def tags=(array_of_tags)
|
50
|
+
tags.replace(array_of_tags)
|
51
|
+
end
|
52
|
+
|
53
|
+
def tag(tag)
|
54
|
+
tags << tag
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,364 @@
|
|
1
|
+
module Virginity
|
2
|
+
|
3
|
+
module FieldCleaning
|
4
|
+
|
5
|
+
def clean!
|
6
|
+
clean_quoted_printable_encoding!
|
7
|
+
clean_base64!
|
8
|
+
clean_binary_data!
|
9
|
+
clean_charsets!
|
10
|
+
guess_latin!
|
11
|
+
remove_encoding_8bit!
|
12
|
+
remove_x_synthesis_ref_params!
|
13
|
+
remove_bom!
|
14
|
+
clean_types!
|
15
|
+
uniq_params!
|
16
|
+
end
|
17
|
+
|
18
|
+
# remove QUOTED-PRINTABLE-encoding
|
19
|
+
#
|
20
|
+
# According to vcard21.doc QUOTED-PRINTABLE cannot occur in structured text and separated text
|
21
|
+
# ... but from experience we know it does.
|
22
|
+
#
|
23
|
+
# Note: reencoding could fail because the characters are not encodable as text
|
24
|
+
LIST_NAMES = %w(CATEGORIES)
|
25
|
+
QUOTED_PRINTABLE = /^quoted-printable$/i
|
26
|
+
ENCODING = /^ENCODING$/i
|
27
|
+
def clean_quoted_printable_encoding!
|
28
|
+
return unless @params.any? {|p| p.key =~ ENCODING and p.value =~ QUOTED_PRINTABLE }
|
29
|
+
if @value.include?(";") # if the unencoded value contains ";" it's a list (or a structured value)
|
30
|
+
v = @value.split(";").map { |e| EncodingDecoding::decode_quoted_printable(e) }
|
31
|
+
@value = EncodingDecoding::encode_text_list(v, ";")
|
32
|
+
elsif LIST_NAMES.include?(@name) or @value.include?(",") # kludge
|
33
|
+
v = @value.split(",").map { |e| EncodingDecoding::decode_quoted_printable(e) }
|
34
|
+
@value = EncodingDecoding::encode_text_list(v, ",")
|
35
|
+
else
|
36
|
+
v = EncodingDecoding::decode_quoted_printable(@value)
|
37
|
+
@value = EncodingDecoding::encode_text(v)
|
38
|
+
end
|
39
|
+
@params.delete_if {|p| p.key =~ ENCODING and p.value =~ QUOTED_PRINTABLE }
|
40
|
+
self
|
41
|
+
end
|
42
|
+
|
43
|
+
# convert BASE64 to b
|
44
|
+
def clean_base64!
|
45
|
+
@params.each do |param|
|
46
|
+
next unless param.key =~ ENCODING and param.value =~ /^base64$/i
|
47
|
+
param.value = "b"
|
48
|
+
end
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
def clean_binary_data!
|
53
|
+
return unless @params.any? {|param| param.key =~ ENCODING and param.value =~ /^b$/i }
|
54
|
+
@value.gsub!(/\s/, '')
|
55
|
+
self
|
56
|
+
end
|
57
|
+
|
58
|
+
def remove_encoding_8bit! # since it's already implicitly encoded in 8 bits...
|
59
|
+
@params.delete_if {|param| param.key =~ ENCODING and param.value =~ /^8BIT$/ }
|
60
|
+
self
|
61
|
+
end
|
62
|
+
|
63
|
+
CHARSET = "CHARSET"
|
64
|
+
def clean_charsets!
|
65
|
+
return unless charset = @params.find { |param| param.key.casecmp(CHARSET) == 0 }
|
66
|
+
@value.encode!(Encoding::UTF_8, charset.value) unless charset.value == "UTF-8"
|
67
|
+
@value = @value.force_encoding(Encoding::UTF_8)
|
68
|
+
@params.delete charset
|
69
|
+
self
|
70
|
+
end
|
71
|
+
|
72
|
+
# Why do we have two boms? well duh, the string could be in either of those encodings!
|
73
|
+
BOM_UTF8 = [65279].pack('U')
|
74
|
+
BOM_BINARY = BOM_UTF8.dup.force_encoding(Encoding::BINARY)
|
75
|
+
def remove_bom!
|
76
|
+
if @value.encoding == Encoding::BINARY
|
77
|
+
@value.gsub!(BOM_BINARY, '')
|
78
|
+
else
|
79
|
+
# if it's not utf-8, it's callers fault.
|
80
|
+
@value.gsub!(BOM_UTF8, '') # remove the BOM
|
81
|
+
end
|
82
|
+
self
|
83
|
+
end
|
84
|
+
|
85
|
+
CASE_SENSITIVE_TYPES = /^(DOM|INTL|POSTAL|PARCEL|HOME|WORK|OTHER|PREF|VOICE|FAX|MSG|CELL|PAGER|BBS|MODEM|CAR|ISDN|VIDEO|AOL|APPLELINK|ATTMAIL|CIS|EWORLD|INTERNET|IBMMAIL|MCIMAIL|POWERSHARE|PRODIGY|TLX|X400|GIF|CGM|WMF|BMP|MET|PMB|DIB|PICT|TIFF|PDF|PS|JPEG|QTIME|MPEG|MPEG2|AVI|WAVE|AIFF|PCM|X509|PGP)$/i
|
86
|
+
TYPE = "TYPE"
|
87
|
+
def clean_types!
|
88
|
+
params(TYPE).each do |type|
|
89
|
+
type.value.upcase! if type.value =~ CASE_SENSITIVE_TYPES
|
90
|
+
end
|
91
|
+
self
|
92
|
+
end
|
93
|
+
|
94
|
+
X_SYNTHESIS_REF = /^X-Synthesis-Ref\d*$/i
|
95
|
+
def remove_x_synthesis_ref_params!
|
96
|
+
@params.delete_if {|p| p.key =~ X_SYNTHESIS_REF or p.value =~ X_SYNTHESIS_REF }
|
97
|
+
self
|
98
|
+
end
|
99
|
+
|
100
|
+
def uniq_params!
|
101
|
+
params.uniq!
|
102
|
+
self
|
103
|
+
end
|
104
|
+
|
105
|
+
def guess_latin!
|
106
|
+
return if @value.valid_encoding?
|
107
|
+
@value.encode!(Encoding::UTF_8, "ISO-8859-1")
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
module VcardCleaning
|
113
|
+
|
114
|
+
# run almost every clean method on whole vcards and on separate fields (FieldCleaning), in a correct order
|
115
|
+
def clean!
|
116
|
+
clean_version!
|
117
|
+
remove_x_abuids!
|
118
|
+
remove_x_irmc_luids!
|
119
|
+
rstrip_text_fields!
|
120
|
+
# strip_structured_fields! # mh, better not do this here. It's too much
|
121
|
+
remove_empty_fields!
|
122
|
+
fields.each { |field| field.clean! }
|
123
|
+
clean_categories!
|
124
|
+
unpack_nicknames!
|
125
|
+
clean_orgs!
|
126
|
+
max_one_name!
|
127
|
+
clean_name!
|
128
|
+
clean_dates!
|
129
|
+
clean_adrs!
|
130
|
+
convert_xabadrs_to_param!
|
131
|
+
convert_xablabels_to_param!
|
132
|
+
remove_duplicate_lines!
|
133
|
+
remove_singleton_groups!
|
134
|
+
clean_same_value_fields!
|
135
|
+
remove_subset_addresses!
|
136
|
+
reset_empty_formatted_name!
|
137
|
+
self
|
138
|
+
end
|
139
|
+
alias_method :super_clean!, :clean!
|
140
|
+
|
141
|
+
# make sure there is exactly one version-field that says "3.0"
|
142
|
+
# and do that in a smart way so the vcard is not changed if it's not nescessary
|
143
|
+
VERSION30 = "VERSION:3.0"
|
144
|
+
VERSION = "VERSION"
|
145
|
+
def clean_version!
|
146
|
+
unless (self/VERSION30).size == 1
|
147
|
+
lines_with_name(VERSION).each {|f| delete_field(f) }
|
148
|
+
self << VERSION30
|
149
|
+
end
|
150
|
+
self
|
151
|
+
end
|
152
|
+
|
153
|
+
X_ABUID = "X-ABUID"
|
154
|
+
# OS-X is not sending those anymore, but existing contact can still contain those fields
|
155
|
+
def remove_x_abuids!
|
156
|
+
lines_with_name(X_ABUID).each {|f| delete_field(f) }
|
157
|
+
self
|
158
|
+
end
|
159
|
+
|
160
|
+
# Sony-Ericsson phones send those. They don't mean anything for us. They are meant for Windows software that syncs the desktop with a phone afaik.
|
161
|
+
X_IRMC_LUID = "X-IRMC-LUID"
|
162
|
+
def remove_x_irmc_luids!
|
163
|
+
lines_with_name(X_IRMC_LUID).each {|f| delete_field(f) }
|
164
|
+
self
|
165
|
+
end
|
166
|
+
|
167
|
+
# why? to remove trailing semicolons like in: "ORG:foo;bar;"
|
168
|
+
def clean_orgs!
|
169
|
+
organizations.each { |org| org.reencode!(:variable_number_of_fields => true) }
|
170
|
+
self
|
171
|
+
end
|
172
|
+
|
173
|
+
# since we could have received a vcard with too many semicolons
|
174
|
+
NAME = 'N'
|
175
|
+
def max_one_name!
|
176
|
+
n_fields = lines_with_name(NAME)
|
177
|
+
return self unless n_fields.size > 1
|
178
|
+
# remove all N fields except the biggest
|
179
|
+
n_fields.delete(n_fields.max_by { |f| f.raw_value.size })
|
180
|
+
delete(*n_fields)
|
181
|
+
self
|
182
|
+
end
|
183
|
+
|
184
|
+
def clean_name!
|
185
|
+
lines_with_name(NAME).each { |n| n.reencode! }
|
186
|
+
self
|
187
|
+
end
|
188
|
+
|
189
|
+
# there's no use in keeping fields without a value
|
190
|
+
def remove_empty_fields!
|
191
|
+
lines.delete_if do |x|
|
192
|
+
x.raw_value.strip.empty? or (x.respond_to? :values and x.values.join.empty?)
|
193
|
+
end
|
194
|
+
self
|
195
|
+
end
|
196
|
+
|
197
|
+
# make one field containing all of values of fields with the same name
|
198
|
+
def clean_multivalue_fields!(name)
|
199
|
+
fields = lines_with_name(name)
|
200
|
+
while fields.size > 1
|
201
|
+
last = fields.pop
|
202
|
+
fields.first.values.concat( last.values.to_a )
|
203
|
+
delete_field(last)
|
204
|
+
end
|
205
|
+
self
|
206
|
+
end
|
207
|
+
|
208
|
+
# concatenate all CATEGORIES-lines, make categories unique and sorted
|
209
|
+
CATEGORIES = "CATEGORIES"
|
210
|
+
def clean_categories!
|
211
|
+
clean_multivalue_fields!(CATEGORIES) # concat
|
212
|
+
if categories = lines_with_name(CATEGORIES).first
|
213
|
+
categories.values = categories.values.map { |c| c.strip }.sort.uniq
|
214
|
+
end
|
215
|
+
self
|
216
|
+
end
|
217
|
+
|
218
|
+
# after unpacking there SHOULD be one field for each value
|
219
|
+
# the order MUST NOT change.
|
220
|
+
def unpack_field!(field)
|
221
|
+
return if field.values.size == 1
|
222
|
+
field.unpacked.each {|f| add_field(f) }
|
223
|
+
delete_field(field)
|
224
|
+
end
|
225
|
+
|
226
|
+
def unpack_list!(list_name)
|
227
|
+
lines_with_name(list_name).each { |field| unpack_field!(field) }
|
228
|
+
remove_empty_fields!
|
229
|
+
self
|
230
|
+
end
|
231
|
+
|
232
|
+
# after unpacking there should be one categories lines per category
|
233
|
+
# the order should not change, but that doesn't actually matter for categories
|
234
|
+
def unpack_categories!
|
235
|
+
unpack_list!(CATEGORIES)
|
236
|
+
end
|
237
|
+
|
238
|
+
# after unpacking there should be one nickname value per NICKNAME
|
239
|
+
# the order should not change.
|
240
|
+
NICKNAME = "NICKNAME"
|
241
|
+
def unpack_nicknames!
|
242
|
+
unpack_list!(NICKNAME)
|
243
|
+
end
|
244
|
+
|
245
|
+
# if there's only one field in a certain group that group can be reset
|
246
|
+
def remove_singleton_groups!
|
247
|
+
groups = fields.map {|f| f.group }.compact!
|
248
|
+
fields.each do |field|
|
249
|
+
next if field.group.nil?
|
250
|
+
field.group = nil if groups.select { |g| g == field.group }.size == 1
|
251
|
+
end
|
252
|
+
self
|
253
|
+
end
|
254
|
+
|
255
|
+
def convert_custom_osx_field_to_param!(fields)
|
256
|
+
fields.each do |custom|
|
257
|
+
unless custom.group.nil?
|
258
|
+
same_group = where(:group => custom.group) - [custom]
|
259
|
+
same_group.each { |field| field.params << custom.to_param }
|
260
|
+
end
|
261
|
+
delete_field(custom)
|
262
|
+
end
|
263
|
+
self
|
264
|
+
end
|
265
|
+
|
266
|
+
def convert_xabadrs_to_param!
|
267
|
+
convert_custom_osx_field_to_param! lines_with_name("X-ABADR")
|
268
|
+
end
|
269
|
+
|
270
|
+
def convert_xablabels_to_param!
|
271
|
+
convert_custom_osx_field_to_param! lines_with_name("X-ABLabel")
|
272
|
+
end
|
273
|
+
|
274
|
+
def remove_duplicate_lines!
|
275
|
+
fields.uniq!
|
276
|
+
self
|
277
|
+
end
|
278
|
+
|
279
|
+
# merge fields with the same value (collect all params in the remaining field)
|
280
|
+
def clean_same_value_fields!
|
281
|
+
fields.each do |upper|
|
282
|
+
next if Vcard::SINGLETON_FIELDS.include?(upper.name)
|
283
|
+
fields.each do |lower|
|
284
|
+
next if lower.object_id == upper.object_id # nescessary
|
285
|
+
next unless lower.name == upper.name # optimisation
|
286
|
+
next unless lower.raw_value == upper.raw_value # optimisation
|
287
|
+
begin
|
288
|
+
# puts "merging #{upper} and #{lower}"
|
289
|
+
upper.merge_with!(lower)
|
290
|
+
delete_field(lower)
|
291
|
+
# puts "merged: #{upper}"
|
292
|
+
rescue # nescessary
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
296
|
+
self
|
297
|
+
end
|
298
|
+
|
299
|
+
# removes fields that are a subset of another existing field
|
300
|
+
#
|
301
|
+
# In the following example, field two is a subset of field one.
|
302
|
+
# one = Field.new("ADR;TYPE=HOME:;;Bickerson Street 4;Dudley Town;;6215 AX;NeverNeverland")
|
303
|
+
# two = Field.new("ADR;TYPE=HOME:;;Bickerson Street 4;Dudley Town;;;")
|
304
|
+
# two.subset_of?(one) ==> true
|
305
|
+
def remove_subsets_of_structured_fields!(fields)
|
306
|
+
fields = fields.dup
|
307
|
+
while field = fields.pop
|
308
|
+
delete_field(field) if fields.any? {|other| field.subset_of?(other) }
|
309
|
+
end
|
310
|
+
self
|
311
|
+
end
|
312
|
+
|
313
|
+
def remove_subset_addresses!
|
314
|
+
remove_subsets_of_structured_fields!(addresses)
|
315
|
+
end
|
316
|
+
|
317
|
+
def reset_empty_formatted_name!
|
318
|
+
name.reset_formatted!
|
319
|
+
self
|
320
|
+
end
|
321
|
+
|
322
|
+
def remove_extra_photos!(p = photos)
|
323
|
+
p.shift
|
324
|
+
p.each { |line| delete_field(line) }
|
325
|
+
self
|
326
|
+
end
|
327
|
+
|
328
|
+
def remove_extra_logos!
|
329
|
+
remove_extra_photos!(logos)
|
330
|
+
end
|
331
|
+
|
332
|
+
RSTRIPPABLE_FIELDS = /^(EMAIL|FN|IMPP|NOTE|TEL|URL)$/i
|
333
|
+
# remove right hand whitespace from the values of all RSTRIPPABLE_FIELDS
|
334
|
+
def rstrip_text_fields!
|
335
|
+
fields.each do |field|
|
336
|
+
field.raw_value.rstrip! if field.name =~ RSTRIPPABLE_FIELDS
|
337
|
+
end
|
338
|
+
self
|
339
|
+
end
|
340
|
+
|
341
|
+
# remove whitespace around parts of a name
|
342
|
+
def strip_structured_fields!
|
343
|
+
(lines_with_name(NAME) + organizations + addresses).each do |field|
|
344
|
+
field.components.each { |component| field.send(component.to_s+'=', field.send(component).strip) }
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
def clean_dates!
|
349
|
+
(birthdays + anniversaries + dates).each do |day|
|
350
|
+
begin
|
351
|
+
day.date = day.date.to_s # normalize the date format
|
352
|
+
rescue => e
|
353
|
+
nil
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
|
358
|
+
def clean_adrs!
|
359
|
+
lines_with_name("ADR").each do |adr|
|
360
|
+
adr.reencode! if EncodingDecoding::decode_text_list(adr.raw_value, ";").size != 7
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|