vobject 0.1.0 → 1.0.2
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 +5 -5
- data/.github/workflows/macos.yml +38 -0
- data/.github/workflows/ubuntu.yml +56 -0
- data/.github/workflows/windows.yml +40 -0
- data/.hound.yml +3 -0
- data/.rubocop.tb.yml +650 -0
- data/.rubocop.yml +1077 -0
- data/Gemfile +1 -1
- data/LICENSE.txt +21 -17
- data/README.adoc +151 -0
- data/Rakefile +1 -1
- data/lib/c.rb +173 -0
- data/lib/error.rb +19 -0
- data/lib/vcalendar.rb +77 -0
- data/lib/vcard.rb +67 -0
- data/lib/vobject.rb +13 -170
- data/lib/vobject/component.rb +87 -36
- data/lib/vobject/parameter.rb +116 -0
- data/lib/vobject/parametervalue.rb +26 -0
- data/lib/vobject/property.rb +134 -55
- data/lib/vobject/propertyvalue.rb +46 -0
- data/lib/vobject/vcalendar/component.rb +106 -0
- data/lib/vobject/vcalendar/grammar.rb +595 -0
- data/lib/vobject/vcalendar/paramcheck.rb +259 -0
- data/lib/vobject/vcalendar/propertyparent.rb +98 -0
- data/lib/vobject/vcalendar/propertyvalue.rb +606 -0
- data/lib/vobject/vcalendar/typegrammars.rb +605 -0
- data/lib/vobject/vcard/v3_0/component.rb +40 -0
- data/lib/vobject/vcard/v3_0/grammar.rb +175 -0
- data/lib/vobject/vcard/v3_0/paramcheck.rb +110 -0
- data/lib/vobject/vcard/v3_0/parameter.rb +17 -0
- data/lib/vobject/vcard/v3_0/property.rb +18 -0
- data/lib/vobject/vcard/v3_0/propertyvalue.rb +401 -0
- data/lib/vobject/vcard/v3_0/typegrammars.rb +425 -0
- data/lib/vobject/vcard/v4_0/component.rb +40 -0
- data/lib/vobject/vcard/v4_0/grammar.rb +224 -0
- data/lib/vobject/vcard/v4_0/paramcheck.rb +269 -0
- data/lib/vobject/vcard/v4_0/parameter.rb +18 -0
- data/lib/vobject/vcard/v4_0/property.rb +63 -0
- data/lib/vobject/vcard/v4_0/propertyvalue.rb +404 -0
- data/lib/vobject/vcard/v4_0/typegrammars.rb +539 -0
- data/lib/vobject/version.rb +1 -1
- data/vobject.gemspec +19 -16
- metadata +81 -26
- data/.travis.yml +0 -5
- data/README.md +0 -94
@@ -0,0 +1,40 @@
|
|
1
|
+
require "vobject/component"
|
2
|
+
require "vobject/vcard/v3_0/property"
|
3
|
+
require "vobject/vcard/v3_0/grammar"
|
4
|
+
require "pp"
|
5
|
+
|
6
|
+
module Vcard::V3_0
|
7
|
+
class Component < Vobject::Component
|
8
|
+
class << self
|
9
|
+
def parse(vcf, strict)
|
10
|
+
hash = Vcard::V3_0::Grammar.new(strict).parse(vcf)
|
11
|
+
comp_name = hash.keys.first
|
12
|
+
new comp_name, hash[comp_name], hash[:errors]
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def raise_invalid_parsing
|
18
|
+
raise "vCard parse failed"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def property_base_class
|
25
|
+
version_class.const_get(:Property)
|
26
|
+
end
|
27
|
+
|
28
|
+
def component_base_class
|
29
|
+
version_class.const_get(:Component)
|
30
|
+
end
|
31
|
+
|
32
|
+
def parameter_base_class
|
33
|
+
version_class.const_get(:Parameter)
|
34
|
+
end
|
35
|
+
|
36
|
+
def version_class
|
37
|
+
Vcard::V3_0
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,175 @@
|
|
1
|
+
require "rsec"
|
2
|
+
require "set"
|
3
|
+
require "uri"
|
4
|
+
require "date"
|
5
|
+
include Rsec::Helpers
|
6
|
+
require "vobject"
|
7
|
+
require "vobject/component"
|
8
|
+
require "vobject/vcard/v3_0/paramcheck"
|
9
|
+
require "vobject/vcard/v3_0/typegrammars"
|
10
|
+
require_relative "../../../c"
|
11
|
+
require_relative "../../../error"
|
12
|
+
|
13
|
+
module Vcard::V3_0
|
14
|
+
class Grammar
|
15
|
+
attr_accessor :strict, :errors
|
16
|
+
class << self
|
17
|
+
def unfold(str)
|
18
|
+
str.gsub(/[\n\r]+[ \t]/, "")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def vobject_grammar
|
23
|
+
# properties with value cardinality 1
|
24
|
+
@cardinality1 = {}
|
25
|
+
@cardinality1[:PARAM] = Set.new [:VALUE]
|
26
|
+
@cardinality1[:PROP] = Set.new [:KIND, :N, :BDAY, :ANNIVERSARY, :GENDER, :PRODID, :REV, :UID]
|
27
|
+
|
28
|
+
group = C::IANATOKEN
|
29
|
+
linegroup = group << "."
|
30
|
+
beginend = /BEGIN/i.r | /END/i.r
|
31
|
+
|
32
|
+
# parameters && parameter types
|
33
|
+
paramname = /ENCODING/i.r | /LANGUAGE/i.r | /CONTEXT/i.r | /TYPE/i.r | /VALUE/i.r | /PREF/i.r
|
34
|
+
otherparamname = C::NAME_VCARD ^ paramname
|
35
|
+
paramvalue = C::QUOTEDSTRING_VCARD.map { |s| s } | C::PTEXT_VCARD.map(&:upcase)
|
36
|
+
|
37
|
+
# prefvalue = /[0-9]{1,2}/i.r | "100".r
|
38
|
+
valuetype = /URI/i.r | /DATE/i.r | /DATE-TIME/i.r | /BINARY/i.r | /PTEXT/i.r
|
39
|
+
# mediaattr = /[!\"#$%&'*+.^A-Z0-9a-z_`i{}|~-]+/.r
|
40
|
+
# mediavalue1 = mediaattr | C::QUOTEDSTRING_VCARD
|
41
|
+
# mediatail = seq(";".r >> mediaattr, "=".r << mediavalue1).map do |a, v|
|
42
|
+
# ";#{a}=#{v}"
|
43
|
+
# end
|
44
|
+
# rfc4288regname = /[A-Za-z0-9!#$&.+^+-]{1,127}/.r
|
45
|
+
# rfc4288typename = rfc4288regname
|
46
|
+
# rfc4288subtypename = rfc4288regname
|
47
|
+
# mediavalue = seq(rfc4288typename << "/".r, rfc4288subtypename, # mediatail.star).map do |t, s, tail|
|
48
|
+
# ret = "#{t}/#{s}"
|
49
|
+
# ret = ret . tail[0] unless tail.empty?
|
50
|
+
# ret
|
51
|
+
# end
|
52
|
+
pvalue_list = (seq(paramvalue << ",".r, lazy { pvalue_list }) & /[;:]/.r).map do |e, list|
|
53
|
+
[e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n"), list].flatten
|
54
|
+
end | (paramvalue & /[;:]/.r).map do |e|
|
55
|
+
[e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n")]
|
56
|
+
end
|
57
|
+
typevaluelist = seq(C::IANATOKEN, ",".r >> lazy { typevaluelist }).map do |t, l|
|
58
|
+
[t.upcase, l].flatten
|
59
|
+
end | C::IANATOKEN.map { |t| [t.upcase] }
|
60
|
+
quoted_string_list = (seq(C::QUOTEDSTRING_VCARD << ",".r, lazy { quoted_string_list }) & /[;:]/.r).map do |e, list|
|
61
|
+
[e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n"), list].flatten
|
62
|
+
end | (C::QUOTEDSTRING_VCARD & /[;:]/.r).map do |e|
|
63
|
+
[e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n")]
|
64
|
+
end
|
65
|
+
|
66
|
+
# fmttypevalue = seq(rfc4288typename, "/", rfc4288subtypename).map(&:join)
|
67
|
+
rfc1766primarytag = /[A-Za-z]{1,8}/.r
|
68
|
+
rfc1766subtag = seq("-", /[A-Za-z]{1,8}/.r) { |a, b| a + b }
|
69
|
+
rfc1766language = seq(rfc1766primarytag, rfc1766subtag.star) do |a, b|
|
70
|
+
a += b[0] unless b.empty?
|
71
|
+
a
|
72
|
+
end
|
73
|
+
|
74
|
+
param = seq(/ENCODING/i.r, "=", /b/.r) do |name, _, val|
|
75
|
+
{ name.upcase.tr("-", "_").to_sym => val }
|
76
|
+
end | seq(/LANGUAGE/i.r, "=", rfc1766language) do |name, _, val|
|
77
|
+
{ name.upcase.tr("-", "_").to_sym => val.upcase }
|
78
|
+
end | seq(/CONTEXT/i.r, "=", /word/.r) do |name, _, val|
|
79
|
+
{ name.upcase.tr("-", "_").to_sym => val.upcase }
|
80
|
+
end | seq(/TYPE/i.r, "=", typevaluelist) do |name, _, val|
|
81
|
+
{ name.upcase.tr("-", "_").to_sym => val }
|
82
|
+
end | seq(/VALUE/i.r, "=", valuetype) do |name, _, val|
|
83
|
+
{ name.upcase.tr("-", "_").to_sym => val }
|
84
|
+
end | /PREF/i.r.map do |_name|
|
85
|
+
# this is likely erroneous use of VCARD 2.1 convention in RFC2739; converting to canonical TYPE=PREF
|
86
|
+
{ TYPE: ["PREF"] }
|
87
|
+
end | seq(otherparamname, "=", pvalue_list) do |name, _, val|
|
88
|
+
val = val[0] if val.length == 1
|
89
|
+
{ name.upcase.tr("-", "_").to_sym => val }
|
90
|
+
end | seq(paramname, "=", pvalue_list) do |name, _, val|
|
91
|
+
parse_err("Violated format of parameter value #{name} = #{val}")
|
92
|
+
end
|
93
|
+
|
94
|
+
params = seq(";".r >> param & ";", lazy { params }) do |p, ps|
|
95
|
+
p.merge(ps) do |key, old, new|
|
96
|
+
if @cardinality1[:PARAM].include?(key)
|
97
|
+
parse_err("Violated cardinality of parameter #{key}")
|
98
|
+
end
|
99
|
+
[old, new].flatten
|
100
|
+
# deal with duplicate properties
|
101
|
+
end
|
102
|
+
end | seq(";".r >> param).map { |e| e[0] }
|
103
|
+
|
104
|
+
contentline = seq(linegroup._?, C::NAME_VCARD, params._? << ":".r,
|
105
|
+
C::VALUE, /(\r|\n|\r\n)/) do |g, name, p, value, _|
|
106
|
+
key = name.upcase.tr("-", "_").to_sym
|
107
|
+
hash = { key => {} }
|
108
|
+
hash[key][:value], errors1 = Typegrammars.typematch(strict, key, p[0], :GENERIC, value, @ctx)
|
109
|
+
errors << errors1
|
110
|
+
hash[key][:group] = g[0] unless g.empty?
|
111
|
+
errors << Paramcheck.paramcheck(strict, key, p.empty? ? {} : p[0], @ctx)
|
112
|
+
hash[key][:params] = p[0] unless p.empty?
|
113
|
+
hash
|
114
|
+
end
|
115
|
+
props = seq(contentline, lazy { props }) do |c, rest|
|
116
|
+
c.merge(rest) do |key, old, new|
|
117
|
+
if @cardinality1[:PROP].include?(key.upcase)
|
118
|
+
parse_err("Violated cardinality of property #{key}")
|
119
|
+
end
|
120
|
+
[old, new].flatten
|
121
|
+
# deal with duplicate properties
|
122
|
+
end
|
123
|
+
end | ("".r & beginend).map { {} }
|
124
|
+
|
125
|
+
calpropname = /VERSION/i.r
|
126
|
+
calprop = seq(linegroup._?, calpropname << ":".r, C::VALUE, /[\r\n]/) do |g, key, value, _|
|
127
|
+
key = key.upcase.tr("-", "_").to_sym
|
128
|
+
hash = { key => {} }
|
129
|
+
hash[key][:value], errors1 = Typegrammars.typematch(strict, key, nil, :VCARD, value, @ctx)
|
130
|
+
errors << errors1
|
131
|
+
hash[key][:group] = g[0] unless g.empty?
|
132
|
+
hash
|
133
|
+
end
|
134
|
+
vobject = seq(linegroup._?, /BEGIN:VCARD[\r\n]/i.r >> calprop, props, linegroup._? << /END:VCARD[\r\n]/i.r) do |(_g, v, rest, _g1)|
|
135
|
+
# TODO what do we do with the groups here?
|
136
|
+
parse_err("Missing VERSION attribute") unless v.has_key?(:VERSION)
|
137
|
+
parse_err("Missing FN attribute") unless rest.has_key?(:FN)
|
138
|
+
parse_err("Missing N attribute") unless rest.has_key?(:N)
|
139
|
+
rest.delete(:END)
|
140
|
+
{ VCARD: v.merge(rest), errors: errors.flatten }
|
141
|
+
end
|
142
|
+
vobject.eof
|
143
|
+
end
|
144
|
+
|
145
|
+
def initialize(strict)
|
146
|
+
self.strict = strict
|
147
|
+
self.errors = []
|
148
|
+
end
|
149
|
+
|
150
|
+
def parse(vobject)
|
151
|
+
@ctx = Rsec::ParseContext.new self.class.unfold(vobject), "source"
|
152
|
+
ret = vobject_grammar._parse @ctx
|
153
|
+
if !ret || Rsec::INVALID[ret]
|
154
|
+
if strict
|
155
|
+
raise @ctx.generate_error "source"
|
156
|
+
else
|
157
|
+
errors << @ctx.generate_error("source")
|
158
|
+
ret = { VCARD: nil, errors: errors.flatten }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
Rsec::Fail.reset
|
162
|
+
ret
|
163
|
+
end
|
164
|
+
|
165
|
+
private
|
166
|
+
|
167
|
+
def parse_err(msg)
|
168
|
+
if strict
|
169
|
+
raise @ctx.report_error msg, "source"
|
170
|
+
else
|
171
|
+
errors << @ctx.report_error(msg, "source")
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require "rsec"
|
2
|
+
require "set"
|
3
|
+
require "uri"
|
4
|
+
require "date"
|
5
|
+
include Rsec::Helpers
|
6
|
+
require "vobject"
|
7
|
+
|
8
|
+
module Vcard::V3_0
|
9
|
+
class Paramcheck
|
10
|
+
class << self
|
11
|
+
def paramcheck(strict, prop, params, ctx)
|
12
|
+
errors = []
|
13
|
+
if params && params[:TYPE]
|
14
|
+
parse_err(strict, errors, "multiple values for :TYPE parameter of #{prop}", ctx) if params[:TYPE].is_a?(Array) && params[:TYPE].length > 1 && prop != :EMAIL && prop != :ADR && prop != :TEL && prop != :LABEL && prop != :IMPP
|
15
|
+
end
|
16
|
+
case prop
|
17
|
+
when :NAME, :PROFILE, :GEO, :PRODID, :URL, :VERSION, :CLASS
|
18
|
+
parse_err(strict, errors, "illegal parameters #{params} given for #{prop}", ctx) unless params.empty?
|
19
|
+
when :CALURI, :CAPURI, :CALADRURI, :FBURL
|
20
|
+
params.each do |key, val|
|
21
|
+
parse_err(strict, errors, "illegal parameter #{key} given for #{prop}", ctx) unless key == :TYPE
|
22
|
+
if params[:TYPE].is_a?(Array)
|
23
|
+
val.each do |v|
|
24
|
+
parse_err(strict, errors, "illegal parameter value #{v} given for parameter #{key} of #{prop}", ctx) unless v == "PREF"
|
25
|
+
end
|
26
|
+
else
|
27
|
+
parse_err(strict, errors, "illegal parameter value #{val} given for parameter #{key} of #{prop}", ctx) unless val == "PREF"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
when :SOURCE
|
31
|
+
params.each do |key, val|
|
32
|
+
parse_err(strict, errors, "illegal parameter #{key} given for #{prop}", ctx) unless key == :VALUE || key == :CONTEXT || key =~ /^x/i
|
33
|
+
parse_err(strict, errors, "illegal value #{val} given for parameter #{key} of #{prop}", ctx) if key == :VALUE && val != "uri"
|
34
|
+
parse_err(strict, errors, "illegal value #{val} given for parameter #{key} of #{prop}", ctx) if key == :CONTEXT && val != "word"
|
35
|
+
end
|
36
|
+
when :FN, :N, :NICKNAME, :MAILER, :TITLE, :ROLE, :ORG, :CATEGORIES, :NOTE, :SORT_STRING
|
37
|
+
params.each do |key, val|
|
38
|
+
parse_err(strict, errors, "illegal parameter #{key} given for #{prop}", ctx) unless key == :VALUE || key == :LANGUAGE || key =~ /^x/i
|
39
|
+
parse_err(strict, errors, "illegal value #{val} given for parameter #{key} of #{prop}", ctx) if key == :VALUE && val != "ptext"
|
40
|
+
end
|
41
|
+
when :TEL, :IMPP, :UID
|
42
|
+
# UID included here per errata
|
43
|
+
params.each_key do |key|
|
44
|
+
parse_err(strict, errors, "illegal parameter #{key} given for #{prop}", ctx) unless key == :TYPE
|
45
|
+
end
|
46
|
+
# we do not check the values of the :TEL :TYPE parameter, because they include ianaToken
|
47
|
+
when :EMAIL
|
48
|
+
params.each_key do |key|
|
49
|
+
parse_err(strict, errors, "illegal parameter #{key} given for #{prop}", ctx) unless key == :TYPE
|
50
|
+
end
|
51
|
+
# we do not check the values of the first :EMAIL :TYPE parameter, because they include ianaToken
|
52
|
+
when :ADR, :LABEL
|
53
|
+
params.each do |key, val|
|
54
|
+
parse_err(strict, errors, "illegal parameter #{key} given for #{prop}", ctx) unless [:VALUE, :LANGUAGE, :TYPE].include? key || key =~ /^x/i
|
55
|
+
parse_err(strict, errors, "illegal value #{val} given for parameter #{key} of #{prop}", ctx) if key == :VALUE && val != "ptext"
|
56
|
+
end
|
57
|
+
# we do not check the values of the :ADR :TYPE parameter, because they include ianaToken
|
58
|
+
when :KEY
|
59
|
+
params.each do |key, val|
|
60
|
+
# VALUE included here per errata
|
61
|
+
parse_err(strict, errors, "illegal parameter #{key} given for #{prop}", ctx) unless [:TYPE, :ENCODING, :VALUE].include? key
|
62
|
+
parse_err(strict, errors, "illegal value #{val} given for parameter #{key} of #{prop}", ctx) if key == :VALUE && val != "binary"
|
63
|
+
end
|
64
|
+
# we do not check the values of the :KEY :TYPE parameter, because they include ianaToken
|
65
|
+
when :PHOTO, :LOGO, :SOUND
|
66
|
+
params.each_key do |key|
|
67
|
+
parse_err(strict, errors, "illegal parameter #{key} given for #{prop}", ctx) unless [:VALUE, :TYPE, :ENCODING].include? key
|
68
|
+
end
|
69
|
+
parse_err(strict, errors, "illegal value #{params[:VALUE]} of :VALUE given for #{prop}", ctx) if params[:VALUE] && params[:VALUE] != "binary" && params[:VALUE] != "uri"
|
70
|
+
parse_err(strict, errors, "illegal value #{params[:ENCODING]} of :ENCODING given for #{prop}", ctx) if params[:ENCODING] && (params[:ENCODING] != "b" || params[:VALUE] == "uri")
|
71
|
+
parse_err(strict, errors, "mandatory parameter of :ENCODING missing for #{prop}", ctx) if !params.has_key?(:ENCODING) && (!params.key?(:VALUE) || params[:VALUE] == "binary")
|
72
|
+
# TODO restriction of :TYPE to image types registered with IANA
|
73
|
+
# TODO restriction of :TYPE to sound types registered with IANA
|
74
|
+
when :BDAY, :REV
|
75
|
+
params.each_key do |key|
|
76
|
+
parse_err(strict, errors, "illegal parameter #{key} given for #{prop}", ctx) unless key == :VALUE
|
77
|
+
end
|
78
|
+
parse_err(strict, errors, "illegal value #{params[:VALUE]} of :VALUE given for #{prop}", ctx) if params[:VALUE] && params[:VALUE] != "date" && params[:VALUE] != "date-time"
|
79
|
+
when :AGENT
|
80
|
+
params.each_key do |key|
|
81
|
+
parse_err(strict, errors, "illegal parameter #{key} given for #{prop}", ctx) unless key == :VALUE
|
82
|
+
end
|
83
|
+
parse_err(strict, errors, "illegal value #{params[:VALUE]} of :VALUE given for #{prop}", ctx) if params[:VALUE] && params[:VALUE] != "uri"
|
84
|
+
when :TZ
|
85
|
+
# example in definition contradicts spec! Spec says :TZ takes no params at all
|
86
|
+
params.each_key do |key|
|
87
|
+
parse_err(strict, errors, "illegal parameter #{key} given for #{prop}", ctx) unless key == :VALUE
|
88
|
+
end
|
89
|
+
parse_err(strict, errors, "illegal value #{params[:VALUE]} of :VALUE given for #{prop}", ctx) if params[:VALUE] && params[:VALUE] != "text"
|
90
|
+
else
|
91
|
+
params.each_key do |key|
|
92
|
+
parse_err(strict, errors, "illegal parameter #{key} given for #{prop}", ctx) unless key == :VALUE || key == :LANGUAGE || key =~ /^x/i
|
93
|
+
parse_err(strict, errors, "illegal value #{val} given for parameter #{key} of #{prop}") if key == :VALUE && val != "ptext"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
errors
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def parse_err(strict, errors, msg, ctx)
|
102
|
+
if strict
|
103
|
+
raise ctx.report_error msg, "source"
|
104
|
+
else
|
105
|
+
errors << ctx.report_error(msg, "source")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "vobject/parameter"
|
2
|
+
|
3
|
+
module Vcard::V3_0
|
4
|
+
class Parameter < Vobject::Parameter
|
5
|
+
def parameter_base_class
|
6
|
+
version_class.const_get(:Parameter)
|
7
|
+
end
|
8
|
+
|
9
|
+
def property_base_class
|
10
|
+
version_class.const_get(:Property)
|
11
|
+
end
|
12
|
+
|
13
|
+
def version_class
|
14
|
+
Vcard::V3_0
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "vobject/property"
|
2
|
+
|
3
|
+
module Vcard::V3_0
|
4
|
+
class Property < Vobject::Property
|
5
|
+
end
|
6
|
+
|
7
|
+
def parameter_base_class
|
8
|
+
version_class.const_get(:Parameter)
|
9
|
+
end
|
10
|
+
|
11
|
+
def property_base_class
|
12
|
+
version_class.const_get(:Property)
|
13
|
+
end
|
14
|
+
|
15
|
+
def version_class
|
16
|
+
Vcard::V3_0
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,401 @@
|
|
1
|
+
require "vobject"
|
2
|
+
require "vobject/propertyvalue"
|
3
|
+
|
4
|
+
module Vcard::V3_0
|
5
|
+
module PropertyValue
|
6
|
+
class Text < Vobject::PropertyValue
|
7
|
+
class << self
|
8
|
+
def escape(x)
|
9
|
+
# temporarily escape \\ as \u007f, which is banned from text
|
10
|
+
x.tr("\\", "\u007f").gsub(/\n/, "\\n").gsub(/,/, "\\,").gsub(/;/, "\\;").gsub(/\u007f/, "\\\\")
|
11
|
+
end
|
12
|
+
|
13
|
+
def listencode(x)
|
14
|
+
ret = if x.is_a?(Array)
|
15
|
+
x.map { |m| Text.escape m }.join(",")
|
16
|
+
elsif x.nil? || x.empty?
|
17
|
+
""
|
18
|
+
else
|
19
|
+
Text.escape x
|
20
|
+
end
|
21
|
+
ret
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(val)
|
26
|
+
self.value = val
|
27
|
+
self.type = "text"
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_s
|
31
|
+
Text.escape value
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_hash
|
35
|
+
value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class ClassValue < Text
|
40
|
+
def initialize(val)
|
41
|
+
self.value = val
|
42
|
+
self.type = "classvalue"
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_hash
|
46
|
+
value
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Profilevalue < Text
|
51
|
+
def initialize(val)
|
52
|
+
self.value = val
|
53
|
+
self.type = "profilevalue"
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_hash
|
57
|
+
value
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Kindvalue < Text
|
62
|
+
def initialize(val)
|
63
|
+
self.value = val
|
64
|
+
self.type = "kindvalue"
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_hash
|
68
|
+
value
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class Ianatoken < Text
|
73
|
+
def initialize(val)
|
74
|
+
self.value = val
|
75
|
+
self.type = "ianatoken"
|
76
|
+
end
|
77
|
+
|
78
|
+
def to_hash
|
79
|
+
value
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
class Binary < Text
|
84
|
+
def initialize(val)
|
85
|
+
self.value = val
|
86
|
+
self.type = "binary"
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_hash
|
90
|
+
value
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class Phonenumber < Text
|
95
|
+
def initialize(val)
|
96
|
+
self.value = val
|
97
|
+
self.type = "phonenumber"
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_hash
|
101
|
+
value
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class Uri < Text
|
106
|
+
def initialize(val)
|
107
|
+
self.value = val
|
108
|
+
self.type = "uri"
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_hash
|
112
|
+
value
|
113
|
+
end
|
114
|
+
|
115
|
+
def to_s
|
116
|
+
value
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
class Float < Vobject::PropertyValue
|
121
|
+
include Comparable
|
122
|
+
def <=>(another)
|
123
|
+
value <=> another.value
|
124
|
+
end
|
125
|
+
|
126
|
+
def initialize(val)
|
127
|
+
self.value = val
|
128
|
+
self.type = "float"
|
129
|
+
end
|
130
|
+
|
131
|
+
def to_s
|
132
|
+
value
|
133
|
+
end
|
134
|
+
|
135
|
+
def to_hash
|
136
|
+
value
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
class Integer < Vobject::PropertyValue
|
141
|
+
include Comparable
|
142
|
+
def <=>(another)
|
143
|
+
value <=> another.value
|
144
|
+
end
|
145
|
+
|
146
|
+
def initialize(val)
|
147
|
+
self.value = val
|
148
|
+
self.type = "integer"
|
149
|
+
end
|
150
|
+
|
151
|
+
def to_s
|
152
|
+
value.to_s
|
153
|
+
end
|
154
|
+
|
155
|
+
def to_hash
|
156
|
+
value
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class Date < Vobject::PropertyValue
|
161
|
+
include Comparable
|
162
|
+
def <=>(another)
|
163
|
+
value <=> another.value
|
164
|
+
end
|
165
|
+
|
166
|
+
def initialize(val)
|
167
|
+
self.value = val
|
168
|
+
self.type = "date"
|
169
|
+
end
|
170
|
+
|
171
|
+
def to_s
|
172
|
+
sprintf("%04d-%02d-%02d", value[:year].to_i, value[:month].to_i, value[:day].to_i)
|
173
|
+
end
|
174
|
+
|
175
|
+
def to_hash
|
176
|
+
value
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
class DateTimeLocal < Vobject::PropertyValue
|
181
|
+
include Comparable
|
182
|
+
def <=>(another)
|
183
|
+
value[:time] <=> another.value[:time]
|
184
|
+
end
|
185
|
+
|
186
|
+
def initialize(val)
|
187
|
+
self.value = val.clone
|
188
|
+
# val consists of :time && :zone values. If :zone is empty, floating local time (i.e. system local time) is assumed
|
189
|
+
self.type = "datetimelocal"
|
190
|
+
val[:sec] += (val[:secfrac].to_f / (10**val[:secfrac].length)) if !val[:secfrac].nil? && !val[:secfrac].empty?
|
191
|
+
value[:time] = if val[:zone].nil? || val[:zone].empty?
|
192
|
+
::Time.local(val[:year], val[:month], val[:day], val[:hour], val[:min], val[:sec])
|
193
|
+
else
|
194
|
+
::Time.utc(val[:year], val[:month], val[:day], val[:hour], val[:min], val[:sec])
|
195
|
+
end
|
196
|
+
value[:origtime] = value[:time]
|
197
|
+
if val[:zone] && val[:zone] != "Z"
|
198
|
+
offset = val[:zone][:hour].to_i * 3600 + val[:zone][:min].to_i * 60
|
199
|
+
offset += val[:zone][:sec].to_i if val[:zone][:sec]
|
200
|
+
offset = -offset if val[:sign] == "-"
|
201
|
+
value[:time] += offset.to_i
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def to_s
|
206
|
+
# ret = sprintf("%04d-%02d-%02dT%02d:%02d:%02d", value[:year], value[:month], value[:day], value[:hour], value[:min], value[:sec])
|
207
|
+
ret = sprintf("%s-%s-%sT%s:%s:%s", value[:year], value[:month], value[:day], value[:hour], value[:min], value[:sec])
|
208
|
+
ret = ret + ",#{value[:secfrac]}" if value[:secfrac]
|
209
|
+
zone = "Z" if value[:zone] && value[:zone] == "Z"
|
210
|
+
zone = "#{value[:zone][:sign]}#{value[:zone][:hour]}:#{value[:zone][:min]}" if value[:zone] && value[:zone].is_a?(Hash)
|
211
|
+
ret = ret + zone
|
212
|
+
ret
|
213
|
+
end
|
214
|
+
|
215
|
+
def to_hash
|
216
|
+
ret = {
|
217
|
+
year: value[:year],
|
218
|
+
month: value[:month],
|
219
|
+
day: value[:day],
|
220
|
+
hour: value[:hour],
|
221
|
+
min: value[:min],
|
222
|
+
sec: value[:sec],
|
223
|
+
}
|
224
|
+
ret[:zone] = value[:zone] if value[:zone]
|
225
|
+
ret
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
class Time < Vobject::PropertyValue
|
230
|
+
def initialize(val)
|
231
|
+
self.value = val
|
232
|
+
self.type = "time"
|
233
|
+
end
|
234
|
+
|
235
|
+
def to_s
|
236
|
+
ret = "#{value[:hour]}:#{value[:min]}:#{value[:sec]}"
|
237
|
+
ret = ret + ".#{value[:secfrac]}" if value[:secfrac]
|
238
|
+
zone = ""
|
239
|
+
zone = "Z" if value[:zone] && value[:zone] == "Z"
|
240
|
+
zone = "#{value[:zone][:sign]}#{value[:zone][:hour]}:#{value[:zone][:min]}" if value[:zone] && value[:zone].is_a?(Hash)
|
241
|
+
ret = ret + zone
|
242
|
+
ret
|
243
|
+
end
|
244
|
+
|
245
|
+
def to_hash
|
246
|
+
value
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
class Utcoffset < Vobject::PropertyValue
|
251
|
+
def initialize(val)
|
252
|
+
self.value = val
|
253
|
+
self.type = "utcoffset"
|
254
|
+
end
|
255
|
+
|
256
|
+
def to_s
|
257
|
+
ret = "#{value[:sign]}#{value[:hour]}:#{value[:min]}"
|
258
|
+
# ret += self.value[:sec] if self.value[:sec]
|
259
|
+
ret
|
260
|
+
end
|
261
|
+
|
262
|
+
def to_hash
|
263
|
+
value
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
class Geovalue < Vobject::PropertyValue
|
268
|
+
def initialize(val)
|
269
|
+
self.value = val
|
270
|
+
self.type = "geovalue"
|
271
|
+
end
|
272
|
+
|
273
|
+
def to_s
|
274
|
+
ret = "#{value[:lat]};#{value[:long]}"
|
275
|
+
ret
|
276
|
+
end
|
277
|
+
|
278
|
+
def to_hash
|
279
|
+
value
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
class Version < Vobject::PropertyValue
|
284
|
+
def initialize(val)
|
285
|
+
self.value = val
|
286
|
+
self.type = "version"
|
287
|
+
end
|
288
|
+
|
289
|
+
def to_s
|
290
|
+
value
|
291
|
+
end
|
292
|
+
|
293
|
+
def to_hash
|
294
|
+
value
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
class Org < Vobject::PropertyValue
|
299
|
+
def initialize(val)
|
300
|
+
self.value = val
|
301
|
+
self.type = "org"
|
302
|
+
end
|
303
|
+
|
304
|
+
def to_s
|
305
|
+
value.map { |m| Text.escape m }.join(";")
|
306
|
+
end
|
307
|
+
|
308
|
+
def to_hash
|
309
|
+
value
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
class Fivepartname < Vobject::PropertyValue
|
314
|
+
def initialize(val)
|
315
|
+
self.value = val
|
316
|
+
self.type = "fivepartname"
|
317
|
+
end
|
318
|
+
|
319
|
+
def to_s
|
320
|
+
ret = Text.listencode value[:surname]
|
321
|
+
ret += ";#{Text.listencode value[:givenname]}" if !value[:givenname].empty? || !value[:middlename].empty? || !value[:honprefix].empty? || !value[:honsuffix].empty?
|
322
|
+
ret += ";#{Text.listencode value[:middlename]}" if !value[:middlename].empty? || !value[:honprefix].empty?
|
323
|
+
ret += ";#{Text.listencode value[:honprefix]}" if !value[:honprefix].empty? || !value[:honsuffix].empty?
|
324
|
+
ret += ";#{Text.listencode value[:honsuffix]}" if !value[:honsuffix].empty?
|
325
|
+
ret
|
326
|
+
end
|
327
|
+
|
328
|
+
def to_hash
|
329
|
+
value
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
class Address < Vobject::PropertyValue
|
334
|
+
def initialize(val)
|
335
|
+
self.value = val
|
336
|
+
self.type = "address"
|
337
|
+
end
|
338
|
+
|
339
|
+
def to_s
|
340
|
+
ret = Text.listencode value[:pobox]
|
341
|
+
ret += ";#{Text.listencode value[:ext]}" if !value[:ext].empty? || !value[:street].empty? || !value[:locality].empty? || !value[:region].empty? || !value[:code].empty? || !value[:country].empty?
|
342
|
+
ret += ";#{Text.listencode value[:street]}" if !value[:street].empty? || !value[:locality].empty? || !value[:region].empty? || !value[:code].empty? || !value[:country].empty?
|
343
|
+
ret += ";#{Text.listencode value[:locality]}" if !value[:locality].empty? || !value[:region].empty? || !value[:code].empty? || !value[:country].empty?
|
344
|
+
ret += ";#{Text.listencode value[:region]}" if !value[:region].empty? || !value[:code].empty? || !value[:country].empty?
|
345
|
+
ret += ";#{Text.listencode value[:code]}" if !value[:code].empty? || !value[:country].empty?
|
346
|
+
ret += ";#{Text.listencode value[:country]}" if !value[:country].empty?
|
347
|
+
ret
|
348
|
+
end
|
349
|
+
|
350
|
+
def to_hash
|
351
|
+
value
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
class Textlist < Vobject::PropertyValue
|
356
|
+
def initialize(val)
|
357
|
+
self.value = val
|
358
|
+
self.type = "textlist"
|
359
|
+
end
|
360
|
+
|
361
|
+
def to_s
|
362
|
+
value.map { |m| Text.escape m }.join(",")
|
363
|
+
end
|
364
|
+
|
365
|
+
def to_hash
|
366
|
+
value
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
class Agent < Vobject::PropertyValue
|
371
|
+
def initialize(val)
|
372
|
+
val[:VCARD].delete(:VERSION)
|
373
|
+
self.value = val
|
374
|
+
self.type = "agent"
|
375
|
+
end
|
376
|
+
|
377
|
+
def to_hash
|
378
|
+
ret = {}
|
379
|
+
value.each do |k, v|
|
380
|
+
ret[k] = {}
|
381
|
+
v.each do |k1, v1|
|
382
|
+
if v1.is_a?(Hash)
|
383
|
+
ret[k][k1] = {}
|
384
|
+
v1.each { |k2, v2| ret[k][k1][k2] = v2.to_hash }
|
385
|
+
else
|
386
|
+
ret[k][k1] = v1
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
ret
|
391
|
+
end
|
392
|
+
|
393
|
+
def to_s
|
394
|
+
ret = Vobject::Component.new(:VCARD, value[:VCARD], []).to_s
|
395
|
+
# spec says that colons must be expected, but none of the examples do
|
396
|
+
ret.gsub(/\n/, "\\n").gsub(/,/, "\\,").gsub(/;/, "\\;")
|
397
|
+
# ret.gsub(/\n/,"\\n").gsub(/:/,"\\:")
|
398
|
+
end
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|