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,177 @@
1
+ module Virginity
2
+
3
+ class Error < StandardError; end
4
+ class InvalidEncoding < Error; end
5
+
6
+ module EncodingDecoding
7
+ extend Encodings
8
+
9
+ # VALUE-CHAR = WSP / VCHAR / NON-ASCII
10
+ WSP = [0x20, 0x09] # WSP = SP / HTAB
11
+ VCHAR = 0x21..0x7E # VCHAR = %x21-7E ; visible (printing) characters
12
+ NONASCII = 0x80..0xFF # NON-ASCII = %x80-FF
13
+
14
+ CR_AND_LF = /\r\n/
15
+ CR = "\r"
16
+ LF = "\n"
17
+ def self.decode_quoted_printable(text)
18
+ text.gsub(CR_AND_LF, LF).gsub(/\=([0-9a-fA-F])?\n\s+([0-9a-fA-F])/, "=\\1\\2").unpack('M*').first
19
+ end
20
+
21
+ QP_ALSO_ENCODE = "\x0A\x20"
22
+ def self.encode_quoted_printable(text, options = {})
23
+ options[:also_encode] ||= QP_ALSO_ENCODE
24
+ # special_chars = /[\t ](?:[\v\t ]|$)|[=\x00-\x08\x0B-\x1F\x7F-\xFF#{options[:also_encode]}]/
25
+ special_chars = /[=\x00-\x08\x0B-\x1F\x7F-\xFF#{options[:also_encode]}]/n
26
+ encoded = to_binary(text).gsub(special_chars) do |char|
27
+ char[0 ... -1] + "=%02X" % char[-1].ord
28
+ end
29
+ fold_quoted_printable(encoded, options[:width] || 76, options[:initial_position])
30
+ end
31
+
32
+ QPCHAR = /[^=]|=[\dABCDEF]{2}/
33
+ QPFOLD = "=\r\n" # only vCard 2.1 uses encode_quoted_printable so we always use windows line endings
34
+ def self.fold_quoted_printable(qp_text, width = 76, initial_position = 0)
35
+ return qp_text unless width > 5
36
+ pos = initial_position.to_i
37
+ scanner = StringScanner.new(qp_text)
38
+ folded = ""
39
+ while !scanner.eos?
40
+ char = scanner.scan(QPCHAR)
41
+ charsize = char.size
42
+ if pos + charsize > width - 3
43
+ folded << QPFOLD
44
+ pos = 0
45
+ end
46
+ folded << char
47
+ pos += charsize
48
+ end
49
+ folded
50
+ end
51
+
52
+
53
+ def self.normalize_newlines!(text)
54
+ text.gsub!(/\r?\n|\r/, "\n")
55
+ end
56
+
57
+ # "text": The "text" value type should be used to identify values that
58
+ # contain human-readable text. The character set and language in which
59
+ # the text is represented is controlled by the charset content-header
60
+ # and the language type parameter and content-header.
61
+ #
62
+ # A formatted text line break in a text value type MUST be represented
63
+ # as the character sequence backslash (ASCII decimal 92) followed by a
64
+ # Latin small letter n (ASCII decimal 110) or a Latin capital letter N
65
+ # (ASCII decimal 78), that is "\n" or "\N".
66
+ #
67
+ # TODO options for saving to ascii (convert to quoted printable) or storing plain utf-8
68
+ ENCODED_LF = "\\n"
69
+ CRLF = CR + LF
70
+ BACKSLASH = "\\"
71
+ COMMA = ","
72
+ SEMICOLON = ";"
73
+ STUFF_TO_ENCODE = /[\n\\\,\;]/
74
+ STUFF_NOT_TO_ENCODE = %r{[^\n\\\,\;]*}
75
+ def self.encode_text(text)
76
+ raise "#{text.inspect} must be a String" unless text.is_a? String
77
+ normalize_newlines!(text)
78
+ encoded = ""
79
+ s = StringScanner.new(text)
80
+ while !s.eos?
81
+ encoded << s.scan(STUFF_NOT_TO_ENCODE)
82
+ # 5.8.4 Backslashes, newlines, and commas must be encoded.
83
+ case x = s.scan(STUFF_TO_ENCODE)
84
+ when LF
85
+ encoded << ENCODED_LF
86
+ when BACKSLASH, COMMA, SEMICOLON
87
+ # RFC2426 tells us to encode ":" too, which is needed for structured text fields
88
+ encoded << BACKSLASH << x
89
+ end
90
+ end
91
+ encoded
92
+ end
93
+
94
+ def self.decode_text(text)
95
+ text.gsub(/\\(.)/) { $1.casecmp('n') == 0 ? LF : $1 }
96
+ end
97
+
98
+ def self.encode_text_list(list, separator = COMMA)
99
+ list.map { |value| encode_text(value) }.join(separator)
100
+ end
101
+
102
+ # # TODO: port to C someday
103
+ # This is the old simple implementation that is easy to port to another language
104
+ # this can be a lot faster if we don't create a new string object for each char
105
+ # def self.decode_text_list(text_list, separator = COMMA)
106
+ # strings = []
107
+ # state = :normal # there are two states: :normal and :escaped
108
+ # string = ""
109
+ # text_list.each_char do |char|
110
+ # if state == :escaped
111
+ # string << (%w(n N).include?(char) ? LF : char)
112
+ # state = :normal
113
+ # else
114
+ # case char
115
+ # when BACKSLASH
116
+ # state = :escaped
117
+ # when separator
118
+ # strings << string
119
+ # string = ""
120
+ # else
121
+ # string << char
122
+ # end
123
+ # end
124
+ # end
125
+ # strings << string
126
+ # strings
127
+ # end
128
+
129
+
130
+ NON_ESCAPE_OR_SEPARATOR_REGEXP = {}
131
+ def self.non_escape_or_separator_regexp(separator)
132
+ NON_ESCAPE_OR_SEPARATOR_REGEXP[separator] ||= %r{[^\\#{separator}\\\\]*}
133
+ end
134
+
135
+ def self.decode_text_list(text_list, separator = COMMA)
136
+ not_special = non_escape_or_separator_regexp(separator)
137
+ list = []
138
+ text = ""
139
+ s = StringScanner.new(text_list)
140
+ while !s.eos?
141
+ text << s.scan(not_special)
142
+ break if s.eos?
143
+ case s.getch
144
+ when BACKSLASH
145
+ char = s.getch
146
+ # what do I do when char is nil? ignore the backslash too? I don't know...
147
+ raise InvalidEncoding, "text list \"#{text_list}\" ends after escape char" if char.nil?
148
+ text << (char.casecmp('n') == 0 ? LF : char)
149
+ when separator
150
+ list << text
151
+ text = ""
152
+ else
153
+ raise InvalidEncoding, "read #{s.matched.inspect} at #{s.pos} in #{s.string.inspect} (#{s.string.size}) using #{not_special.inspect}"
154
+ end
155
+ end
156
+ list << text
157
+ list
158
+ end
159
+
160
+ # Compound type values are delimited by a field delimiter, specified by the SEMI-COLON character (ASCII decimal 59). A SEMI-COLON in a component of a compound property value MUST be escaped with a BACKSLASH character (ASCII decimal 92).
161
+ #
162
+ # Lists of values are delimited by a list delimiter, specified by the COMMA character (ASCII decimal 44). A COMMA character in a value MUST be escaped with a BACKSLASH character (ASCII decimal 92).
163
+ #
164
+ # This profile supports the type grouping mechanism defined in [MIME-DIR]. Grouping of related types is a useful technique to communicate common semantics concerning the properties of a vCard.
165
+ def self.decode_structured_text(value, size, separator = SEMICOLON)
166
+ list = decode_text_list(value, separator)
167
+ list << "" while list.size < size
168
+ list.pop while list.size > size
169
+ list
170
+ end
171
+
172
+ def self.encode_structured_text(list, separator = SEMICOLON)
173
+ encode_text_list(list, separator)
174
+ end
175
+
176
+ end
177
+ end
@@ -0,0 +1,36 @@
1
+ module Virginity
2
+ module Encodings
3
+
4
+ def binary?(s)
5
+ s.encoding == Encoding::BINARY
6
+ end
7
+
8
+ def to_binary(s)
9
+ s.dup.force_encoding(Encoding::BINARY)
10
+ end
11
+
12
+ def to_ascii(s)
13
+ s.dup.force_encoding(Encoding::ASCII)
14
+ end
15
+
16
+ def to_default(s)
17
+ s.encode
18
+ end
19
+
20
+ def to_default!(s)
21
+ s.encode!
22
+ end
23
+
24
+ def verify_utf8ness(string)
25
+ if string.encoding == Encoding::UTF_8 || string.encoding == Encoding::US_ASCII
26
+ unless string.valid_encoding?
27
+ # puts "*"*100, "incorrectly encoded String", string, "*"*100
28
+ raise InvalidEncoding, "incorrectly encoded String"
29
+ end
30
+ else
31
+ raise InvalidEncoding, "expected UTF-8 or ASCII"
32
+ end
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,230 @@
1
+ module Virginity
2
+ module Fixes
3
+ def unfold_faulty_qp_lines(faulty_lines)
4
+ lines = faulty_lines.dup
5
+ loop do # unfold line that do not begin with " " but are encoded as QP
6
+ changed = false
7
+ lines.each_with_index do |line, i|
8
+ # if line is QP, ends with equals and is not the last line
9
+ if line =~ /ENCODING=QUOTED-PRINTABLE/i and line =~ /=$/ and lines.length > i+1
10
+ lines[i] = lines[i].chomp('=') # remove the soft-line break, the last character, (=)...
11
+ lines[i] += lines.delete_at(i+1) # ...and add the next line to the failing line.
12
+ changed = true
13
+ end
14
+ end
15
+ break unless changed
16
+ end
17
+ lines
18
+ end
19
+
20
+ def non_empty_lines(faulty_lines) # (needed for simon3.vcf)
21
+ faulty_lines.reject {|line| line.empty? }
22
+ end
23
+
24
+ # for osx
25
+ # more info on this Base64-line: http://www.imc.org/imc-vcard/mail-archive/msg00555.html
26
+ def remove_spaces_from_base64_lines(faulty_lines)
27
+ faulty_lines.map do |line|
28
+ if line =~ /BASE64/ # FIXME, this will break if the value contains this string. --> so let's do this in the field-class?
29
+ line.gsub(/\s/, "")
30
+ else
31
+ line
32
+ end
33
+ end
34
+ end
35
+
36
+ def self.line_parts(line)
37
+ ContentLine::line_parts(line)
38
+ rescue InvalidEncoding
39
+ # so, it's invalid 3.0 encoded, could it be a 2.1-encoding?
40
+ DirectoryInformation::line21_parts(line)
41
+ end
42
+
43
+ # FIXME --> width can't be greater than width in the normal folding method... but it is now :-(
44
+ def self.photo_folding_like_apple(value, options = {})
45
+ width = options[:width] || 78
46
+ line_ending = (options[:windows_line_endings] ? "\r\n" : "\n")
47
+ s = line_ending + " " + value.gsub(/.{#{width-2},#{width-2}}/) {|x| x + line_ending + " "}
48
+ s.sub(/#{line_ending} $/,"") # remove the last line ending if if it so happens to be that the last line is 'empty'
49
+ end
50
+
51
+ def self.sane_line_endings(s)
52
+ s.gsub(LineFolding::LINE_ENDING, "\n")
53
+ end
54
+
55
+ def self.should_be_folded?(line)
56
+ return false if line =~ /\A $/ # TODO: clarify this, it was line.first, which takes the first string.
57
+ if line.include?(":") # probably a field
58
+ unless line[0] == ":" or line.split(":").first.match(/[\(\)]/)
59
+ # it is a name then, so it's a new field
60
+ return false
61
+ end
62
+ end
63
+ true
64
+ end
65
+
66
+ def self.unfold_wrongly_folded_lines(s)
67
+ x = ""
68
+ sane_line_endings(s).split("\n").each do |line|
69
+ if should_be_folded?(line.dup)
70
+ x << " " + line
71
+ else
72
+ x << line
73
+ end
74
+ x << "\n"
75
+ end
76
+ x
77
+ end
78
+
79
+ def self.remove_ascii_ctl_chars(s)
80
+ ctl = (0..31).to_a + [127]
81
+ v = ""
82
+ s.each_byte do |c|
83
+ v << c unless ctl.include? c
84
+ end
85
+ v
86
+ end
87
+
88
+ def self.reencode_qp(qp, type=:value)
89
+ decoded = EncodingDecoding::decode_quoted_printable(qp)
90
+ case type
91
+ when nil
92
+ @value = EncodingDecoding::encode_text decoded
93
+ when :separated
94
+ @value = EncodingDecoding::encode_text_list [decoded]
95
+ when :structured
96
+ @value = EncodingDecoding::encode_structured_text [decoded]
97
+ else
98
+ raise TypeError, type
99
+ end
100
+ end
101
+
102
+ EQUALS_SIGN_WITHOUT_HEX = /=(([^0-9^A-F])|(.[^0-9^A-F]))/
103
+
104
+ def self.fix_faulty_qp_chars(s)
105
+ x = Virginity::LineFolding::unfold_and_split(s)
106
+ y = x.map do |l|
107
+ if l =~ /QUOTED\-PRINTABLE/
108
+ l =~ /(.*):(.*)/
109
+ prevalue, value = $1, $2
110
+ value = value.gsub(EQUALS_SIGN_WITHOUT_HEX) { |s| " #{$1}" }
111
+ "#{prevalue}:#{value}"
112
+ else
113
+ l
114
+ end
115
+ end
116
+ y.join("\n")
117
+ end
118
+
119
+ def self.guess_latin1(s)
120
+ x = Vcard.new(s).super_clean!
121
+ x.fields.each do |f|
122
+ begin
123
+ f.to_s.encode('UTF-8//TRANSLIT', Encoding::UTF_8)
124
+ f.to_s.force_encoding(LATIN1).encode
125
+ rescue EncodingError
126
+ print "\tguessing Latin1 for #{f.to_s.inspect}"
127
+ f.params << Param.new("CHARSET", "Latin1")
128
+ f.clean_charsets!
129
+ raise "GAAAAAAAAH!" unless f.to_s.is_utf8?
130
+ puts "\t-->\t" + f.to_s.inspect
131
+ end
132
+ end
133
+ x.super_clean!.to_s
134
+ end
135
+
136
+ # def faulty?(lns=lines)
137
+ # lns.any? { |line| not line =~ %r{#{Bnf::LINE}}i }
138
+ # end
139
+
140
+ # def fix!(lines)
141
+ # ls = lines.dup
142
+ # ls = unfold_faulty_qp_lines(ls)
143
+ # ls = non_empty_lines(ls)
144
+ # raise InvalidEncoding.new("I do not know how to repair this vcard:\n" + inspect) if faulty?(ls)
145
+ # ls
146
+ # end
147
+
148
+ # def fixed!
149
+ # original = lines.join("\n")
150
+ # ls = lines.dup
151
+ # before = original.dup
152
+ # after = ""
153
+ # while before != after and faulty_lines?(lines)
154
+ # before = ls.join("\n")
155
+ # ls = unfold_faulty_qp_lines(ls)
156
+ # ls = non_empty_lines(ls)
157
+ # after = ls.join("\n")
158
+ # end
159
+ # raise InvalidEncoding.new("I do not know how to repair this vcard:\n" + original) if faulty_lines?(lines)
160
+ # lines
161
+ # end
162
+
163
+ end
164
+ end
165
+
166
+
167
+ class Virginity::Vcard < Virginity::DirectoryInformation
168
+
169
+ def self.fields_from_broken_vcard(vcard_as_string)
170
+ lines = Virginity::LineFolding::unfold_and_split(vcard_as_string.lstrip).map do |line|
171
+ begin
172
+ # if it is not valid 3.0, could it be a 2.1-line?
173
+ Virginity::Field.parse(line)
174
+ rescue
175
+ group, name, params, value = DirectoryInformation::line21_parts(line)
176
+ Virginity::Field[name].new(name, value, params, group, :no_deep_copy => true)
177
+ end
178
+ end
179
+ end
180
+
181
+ def self.valid_utf8?(v)
182
+ if v.to_s.dup.force_encoding(Encoding::UTF_8).valid_encoding?
183
+ true
184
+ else
185
+ false
186
+ end
187
+ rescue EncodingError
188
+ return false
189
+ end
190
+
191
+ # NB. this only works for vCard 3.0, not for 2.1!
192
+ def self.fix_and_clean(vcard_as_string)
193
+ fixes ||= []
194
+ # puts vcard_as_string
195
+ # puts "fixes => #{fixes.inspect}"
196
+ lines = fields_from_broken_vcard(vcard_as_string)
197
+ v = Virginity::Vcard.new(lines)
198
+ v.super_clean!
199
+ valid_utf8?(v)
200
+ v
201
+ rescue EncodingError
202
+ #puts e.class, e
203
+ if !fixes.include?(:faulty_qp)
204
+ newcard = Fixes::fix_faulty_qp_chars(vcard_as_string)
205
+ fixes << :faulty_qp
206
+ elsif !fixes.include?(:guess_latin1)
207
+ newcard = Fixes::guess_latin1(lines)
208
+ fixes << :guess_latin1
209
+ else
210
+ File.open("illegal_sequence_#{vcard_as_string.hash}.vcf", "wb") do |f|
211
+ f.puts Virginity::Vcard.new(vcard_as_string).super_clean!
212
+ end
213
+ raise
214
+ end
215
+ vcard_as_string = newcard
216
+ retry
217
+ rescue Virginity::InvalidEncoding, Virginity::Vcard21::ParseError => e
218
+ if !fixes.include?(:fix_folding)
219
+ vcard_as_string = Fixes::unfold_wrongly_folded_lines(vcard_as_string)
220
+ fixes << :fix_folding
221
+ retry
222
+ elsif !fixes.include?(:weird_mac_16bit_encoding_that_is_not_utf16) && vcard_as_string.include?("\000")
223
+ vcard_as_string.gsub!("\000", "")
224
+ fixes << :weird_mac_16bit_encoding_that_is_not_utf16
225
+ retry
226
+ else
227
+ raise
228
+ end
229
+ end
230
+ end
@@ -0,0 +1,244 @@
1
+ require 'digest'
2
+ require 'virginity/encodings'
3
+ require 'virginity/dir_info'
4
+ require 'virginity/vcard/cleaning'
5
+ require 'virginity/vcard/categories'
6
+ require 'virginity/vcard/name_handler'
7
+ require 'virginity/vcard/fields'
8
+ require 'virginity/vcard/fields_osx'
9
+ require 'virginity/vcard/fields_soocial'
10
+ require 'virginity/vcard/patching'
11
+
12
+ module Virginity
13
+
14
+ class InvalidVcard < Error
15
+ attr_reader :original
16
+
17
+ def initialize(msg, original = $!)
18
+ super(msg)
19
+ @original = original
20
+ end
21
+ end
22
+
23
+ # rfc 2426, vCard MIME Directory Profile
24
+ class Vcard < DirectoryInformation
25
+ include VcardCleaning
26
+ include VcardCategories
27
+ include Patching
28
+ include Encodings
29
+
30
+ def self.diff(old, new)
31
+ Patching::diff(old, new)
32
+ end
33
+
34
+ EMPTY = "BEGIN:VCARD\nN:;;;;\nVERSION:3.0\nEND:VCARD\n"
35
+
36
+ VCARD_REGEX = /^[\ \t]*VCARD[\ \t]*$/i
37
+ def initialize(lines = EMPTY, options = {})
38
+ if lines.is_a? Array
39
+ @lines = lines.map {|line| line.to_field }
40
+ else # it's expected to be a String
41
+ verify_utf8ness lines
42
+ @lines = LineFolding::unfold_and_split(lines.lstrip).map { |line| Field.parse(line) }
43
+ end
44
+ raise InvalidVcard, "missing BEGIN:VCARD" unless @lines.first.name =~ Field::BEGIN_REGEX
45
+ raise InvalidVcard, "missing END:VCARD" unless @lines.last.name =~ Field::END_REGEX
46
+ raise InvalidVcard, "missing BEGIN:VCARD" unless @lines.first.raw_value =~ VCARD_REGEX
47
+ raise InvalidVcard, "missing END:VCARD" unless @lines.last.raw_value =~ VCARD_REGEX
48
+ rescue => error
49
+ raise InvalidVcard, error.message
50
+ end
51
+
52
+ # the empty vcard
53
+ def self.empty
54
+ new(LineFolding::unfold_and_split(EMPTY.lstrip).map { |line| Field.parse(line) })
55
+ end
56
+
57
+ # import vcard21 and convert it to 3.0
58
+ def self.from_vcard21(vcf, options = {})
59
+ verify_utf8ness vcf
60
+ vcard = new(lines_from_vcard21(vcf, options), options)
61
+ vcard.clean_version!
62
+ rescue => error
63
+ raise InvalidVcard, error.message
64
+ end
65
+
66
+ VERSION21 = /^VERSION\:2\.1(\s*)$/i
67
+ def self.parse(vcf, options = {})
68
+ vcf = vcf.to_s
69
+ verify_utf8ness vcf
70
+ if vcf =~ VERSION21
71
+ from_vcard21(vcf, options)
72
+ else
73
+ new(vcf, options)
74
+ end
75
+ end
76
+
77
+ def self.from_vcard(vcf, options = {})
78
+ parse(vcf, options)
79
+ end
80
+
81
+ def self.load_all_from(filename, options = {})
82
+ list(File.read(filename), options)
83
+ end
84
+
85
+ END_VCARD = /^END\:VCARD\s*$/i
86
+ # split a given string of concatenated vcards to an array containing one vcard per element
87
+ def self.vcards_in_list(vcf)
88
+ s = StringScanner.new(vcf)
89
+ array = []
90
+ while !s.eos?
91
+ if v = s.scan_until(END_VCARD)
92
+ v.lstrip!
93
+ array << v
94
+ else
95
+ puts s.peek(100)
96
+ break
97
+ end
98
+ end
99
+ array
100
+ end
101
+
102
+ # returns an array of Vcards for a string of concatenated vcards
103
+ def self.list(vcf, options = {})
104
+ vcards_in_list(vcf).map do |v|
105
+ from_vcard(v, options)
106
+ end
107
+ end
108
+
109
+ def inspect
110
+ super.chomp(">") + " name=" + name.to_s.inspect + ">"
111
+ end
112
+
113
+ def dir_info
114
+ DirectoryInformation.new(to_s)
115
+ end
116
+
117
+ def deep_copy
118
+ Marshal::load(Marshal::dump(self))
119
+ end
120
+
121
+ alias_method :fields, :lines
122
+ alias_method :delete_field, :delete_content_line
123
+
124
+ # add a field and return it
125
+ # if a block is given, then the new field is yielded so it can be changed
126
+ def add_field(line)
127
+ end_vcard = @lines.pop
128
+ raise InvalidVcard, "there is no last line? ('END:VCARD')" if end_vcard.nil?
129
+ @lines << (field = Field.parse(line))
130
+ @lines << end_vcard
131
+ yield field if block_given?
132
+ field
133
+ end
134
+
135
+ # add a field, returns the vcard
136
+ def <<(line)
137
+ add_field(line)
138
+ self
139
+ end
140
+ alias_method :push, :<<
141
+
142
+ # a vCard 2.1 string representation of this vCard
143
+ # if :windows_line_endings => true then lines end with \r\n otherwise with \n
144
+ CRLF = "\r\n"
145
+ def to_vcard21(options = {})
146
+ line_ending = options[:windows_line_endings] ? CRLF : LF
147
+ fields.map { |field| field.encode21(options) }.join(line_ending)
148
+ end
149
+
150
+ # are all fields also present or supersets in other?
151
+ IGNORE_IN_SUBSET_COMPARISON = %w(BEGIN END FN)
152
+ def subset_of?(other)
153
+ fields.all? do |field|
154
+ if IGNORE_IN_SUBSET_COMPARISON.include?(field.name)
155
+ true
156
+ else
157
+ # puts "-----\n"
158
+ # puts "#{field} in #{other}?\n"
159
+ other.lines_with_name(field.name).any? do |f|
160
+ begin
161
+ # puts "considering #{f} ==> #{f.raw_value == field.raw_value or field.subset_of?(f)}"
162
+ f.raw_value == field.raw_value or field.subset_of?(f)
163
+ rescue NoMethodError
164
+ false
165
+ end
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ SINGLETON_FIELDS = %w(N FN BEGIN END VERSION)
172
+ # import all fields except N, FN, and VERSION from other (another Vcard)
173
+ # duplicate fields are deduped
174
+ def assimilate_fields_from!(other)
175
+ other.fields.each do |field|
176
+ next if SINGLETON_FIELDS.include? field.name.upcase
177
+ push(field)
178
+ end
179
+ clean_same_value_fields!
180
+ self
181
+ end
182
+
183
+ def add(name)
184
+ add_field(name + ":") do |field|
185
+ yield field if block_given?
186
+ end
187
+ end
188
+
189
+ def add_email(address = nil)
190
+ add(EMAIL) do |email|
191
+ email.address = address.to_s
192
+ yield email if block_given?
193
+ end
194
+ end
195
+
196
+ def add_telephone(number = nil)
197
+ add(TEL) do |tel|
198
+ tel.number = number.to_s
199
+ yield tel if block_given?
200
+ end
201
+ end
202
+
203
+ def name
204
+ @name ||= NameHandler.new(self)
205
+ end
206
+
207
+ ADR = "ADR"
208
+ BDAY = "BDAY"
209
+ CATEGORIES = "CATEGORIES"
210
+ EMAIL = "EMAIL"
211
+ IMPP = "IMPP"
212
+ LOGO = "LOGO"
213
+ NICKNAME = "NICKNAME"
214
+ TEL = "TEL"
215
+ NOTE = "NOTE"
216
+ ORG = "ORG"
217
+ PHOTO = "PHOTO"
218
+ TITLE = "TITLE"
219
+ URL = "URL"
220
+ def addresses; lines_with_name(ADR); end
221
+ def birthdays; lines_with_name(BDAY); end
222
+ def categories; lines_with_name(CATEGORIES); end
223
+ def emails; lines_with_name(EMAIL); end
224
+ def impps; lines_with_name(IMPP); end
225
+ def logos; lines_with_name(LOGO); end
226
+ def nicknames; lines_with_name(NICKNAME); end
227
+ def telephones; lines_with_name(TEL); end
228
+ def notes; lines_with_name(NOTE); end
229
+ def organizations; lines_with_name(ORG); end
230
+ alias_method :organisations, :organizations # Britania rules the waves!
231
+ def photos; lines_with_name(PHOTO); end
232
+ def titles; lines_with_name(TITLE); end
233
+ def urls; lines_with_name(URL); end
234
+ def custom_im_fields; @lines.select{|line| line.is_a? Virginity::Vcard::CustomImField}; end
235
+
236
+ XABRELATEDNAMES = "X-ABRELATEDNAMES"
237
+ def related_names; lines_with_name(XABRELATEDNAMES); end
238
+ XABDATE = "X-ABDATE"
239
+ def dates; lines_with_name(XABDATE); end
240
+ XANNIVERSARY = "X-ANNIVERSARY"
241
+ def anniversaries; lines_with_name(XANNIVERSARY); end
242
+ end
243
+
244
+ end