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.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/macos.yml +38 -0
  3. data/.github/workflows/ubuntu.yml +56 -0
  4. data/.github/workflows/windows.yml +40 -0
  5. data/.hound.yml +3 -0
  6. data/.rubocop.tb.yml +650 -0
  7. data/.rubocop.yml +1077 -0
  8. data/Gemfile +1 -1
  9. data/LICENSE.txt +21 -17
  10. data/README.adoc +151 -0
  11. data/Rakefile +1 -1
  12. data/lib/c.rb +173 -0
  13. data/lib/error.rb +19 -0
  14. data/lib/vcalendar.rb +77 -0
  15. data/lib/vcard.rb +67 -0
  16. data/lib/vobject.rb +13 -170
  17. data/lib/vobject/component.rb +87 -36
  18. data/lib/vobject/parameter.rb +116 -0
  19. data/lib/vobject/parametervalue.rb +26 -0
  20. data/lib/vobject/property.rb +134 -55
  21. data/lib/vobject/propertyvalue.rb +46 -0
  22. data/lib/vobject/vcalendar/component.rb +106 -0
  23. data/lib/vobject/vcalendar/grammar.rb +595 -0
  24. data/lib/vobject/vcalendar/paramcheck.rb +259 -0
  25. data/lib/vobject/vcalendar/propertyparent.rb +98 -0
  26. data/lib/vobject/vcalendar/propertyvalue.rb +606 -0
  27. data/lib/vobject/vcalendar/typegrammars.rb +605 -0
  28. data/lib/vobject/vcard/v3_0/component.rb +40 -0
  29. data/lib/vobject/vcard/v3_0/grammar.rb +175 -0
  30. data/lib/vobject/vcard/v3_0/paramcheck.rb +110 -0
  31. data/lib/vobject/vcard/v3_0/parameter.rb +17 -0
  32. data/lib/vobject/vcard/v3_0/property.rb +18 -0
  33. data/lib/vobject/vcard/v3_0/propertyvalue.rb +401 -0
  34. data/lib/vobject/vcard/v3_0/typegrammars.rb +425 -0
  35. data/lib/vobject/vcard/v4_0/component.rb +40 -0
  36. data/lib/vobject/vcard/v4_0/grammar.rb +224 -0
  37. data/lib/vobject/vcard/v4_0/paramcheck.rb +269 -0
  38. data/lib/vobject/vcard/v4_0/parameter.rb +18 -0
  39. data/lib/vobject/vcard/v4_0/property.rb +63 -0
  40. data/lib/vobject/vcard/v4_0/propertyvalue.rb +404 -0
  41. data/lib/vobject/vcard/v4_0/typegrammars.rb +539 -0
  42. data/lib/vobject/version.rb +1 -1
  43. data/vobject.gemspec +19 -16
  44. metadata +81 -26
  45. data/.travis.yml +0 -5
  46. data/README.md +0 -94
@@ -0,0 +1,26 @@
1
+ module Vobject
2
+ class ParameterValue
3
+ def initialize(val)
4
+ self.value = val
5
+ end
6
+ # raise_invalid_initialization if key != name
7
+ end
8
+
9
+ def to_s
10
+ value
11
+ end
12
+
13
+ private
14
+
15
+ def name
16
+ prop_name
17
+ end
18
+
19
+ def default_value_type
20
+ "text"
21
+ end
22
+
23
+ def raise_invalid_initialization
24
+ raise "vObject property initialization failed"
25
+ end
26
+ end
@@ -1,83 +1,162 @@
1
- module Vobject
2
-
3
- class Property
4
-
5
- MAX_LINE_WIDTH = 75
6
-
7
- attr_accessor :group, :prop_name, :params, :value
1
+ require "vobject"
2
+ require "vobject/parameter"
3
+
4
+ class Vobject::Property
5
+ attr_accessor :group, :prop_name, :params, :value, :multiple, :norm
6
+
7
+ def <=>(another)
8
+ if self.prop_name =~ /^VERSION$/i
9
+ -1
10
+ elsif another.prop_name =~ /^VERSION$/i
11
+ 1
12
+ else
13
+ self.to_norm <=> another.to_norm
14
+ end
15
+ end
8
16
 
9
- def initialize key, options
10
- self.group = options[:group]
17
+ def initialize(key, options)
18
+ if options.class == Array
19
+ self.multiple = []
20
+ options.each do |v|
21
+ multiple << property_base_class.new(key, v)
22
+ self.prop_name = key
23
+ end
24
+ else
11
25
  self.prop_name = key
12
- self.params = options[:params]
13
- self.value = parse_value(options[:value])
26
+ if options.nil? || options.empty?
27
+ self.group = nil
28
+ self.params = []
29
+ self.value = nil
30
+ else
31
+ self.group = options[:group]
32
+ self.prop_name = key
33
+ unless options[:params].nil? || options[:params].empty?
34
+ self.params = []
35
+ options[:params].each do |k, v|
36
+ params << parameter_base_class.new(k, v)
37
+ end
38
+ end
39
+ # self.value = parse_value(options[:value])
40
+ self.value = options[:value]
41
+ end
42
+ end
43
+ self.norm = nil
44
+ raise_invalid_initialization if key != name
45
+ end
14
46
 
15
- raise_invalid_initialization if key != name
47
+ def to_s
48
+ if multiple.nil? || multiple.empty?
49
+ ret = to_s_line
50
+ else
51
+ arr = []
52
+ multiple.each do |x|
53
+ arr << x.to_s_line
54
+ end
55
+ ret = arr.join("")
16
56
  end
57
+ ret
58
+ end
17
59
 
18
- def to_s
19
- line = group ? "#{group}." : ""
20
- line << "#{name}"
60
+ def to_s_line
61
+ line = group ? "#{group}." : ""
62
+ line << name.to_s.tr("_", "-")
21
63
 
22
- (params || {}).each do |pname, pvalue|
23
- pvalue.to_s.gsub!(/\n/, '\n')
64
+ (params || {}).each do |p|
65
+ line << ";#{p}"
66
+ end
24
67
 
25
- line << ";#{pname}=#{pvalue}"
26
- end
68
+ line << ":#{value}"
27
69
 
28
- line << ":#{value}"
70
+ line = Vobject::fold_line(line) << "\n"
29
71
 
30
- line = fold_line(line) << "\n"
72
+ line
73
+ end
31
74
 
32
- line
75
+ def to_norm
76
+ if @norm.nil?
77
+ if multiple.nil? || multiple.empty?
78
+ ret = to_norm_line
79
+ else
80
+ arr = []
81
+ multiple.sort.each do |x|
82
+ arr << x.to_norm_line
83
+ end
84
+ ret = arr.join("")
85
+ end
86
+ @norm = ret
33
87
  end
88
+ @norm
89
+ end
34
90
 
35
- private
91
+ def to_norm_line
92
+ line = group ? "#{group}." : ""
93
+ line << name.to_s.tr("_", "-").upcase
36
94
 
37
- def name
38
- prop_name
95
+ (params || {}).sort.each do |p|
96
+ line << ";#{p.to_norm}"
39
97
  end
40
98
 
41
- def parse_value value
42
- parse_method = :"parse_#{value_type}_value"
43
- parse_method = respond_to?(parse_method, true) ? parse_method : :parse_text_value
44
- send(parse_method, value)
45
- end
99
+ line << ":#{value.to_norm}"
46
100
 
47
- def parse_text_value value
48
- value
49
- end
101
+ line = Vobject::fold_line(line) << "\n"
50
102
 
51
- def value_type
52
- (params || {})[:VALUE] || default_value_type
53
- end
103
+ line
104
+ end
54
105
 
55
- def default_value_type
56
- "text"
57
- end
58
106
 
59
- def raise_invalid_initialization
60
- raise "vObject property initialization failed"
107
+ def to_hash
108
+ ret = {}
109
+ if multiple
110
+ ret[prop_name] = []
111
+ multiple.each do |c|
112
+ ret[prop_name] = ret[prop_name] << c.to_hash[prop_name]
113
+ end
114
+ else
115
+ ret = {prop_name => { value: value.to_hash } }
116
+ ret[prop_name][:group] = group unless group.nil?
117
+ if params
118
+ ret[prop_name][:params] = {}
119
+ params.each do |p|
120
+ ret[prop_name][:params] = ret[prop_name][:params].merge p.to_hash
121
+ end
122
+ end
61
123
  end
124
+ ret
125
+ end
62
126
 
63
- # This implements the line folding as specified in
64
- # http://tools.ietf.org/html/rfc6350#section-3.2
65
- #
66
- # NOTE: the "line" here is not including the trailing \n
67
- def fold_line(line)
68
- folded_line = line[0, MAX_LINE_WIDTH]
69
- remainder_line = line[MAX_LINE_WIDTH, line.length - MAX_LINE_WIDTH] || ''
127
+ private
70
128
 
71
- max_width = MAX_LINE_WIDTH - 1
129
+ def name
130
+ prop_name
131
+ end
72
132
 
73
- for i in 0..((remainder_line.length - 1) / max_width)
74
- folded_line << "\n "
75
- folded_line << remainder_line[i * max_width, max_width]
76
- end
133
+ def parse_value(value)
134
+ parse_method = :"parse_#{value_type}_value"
135
+ parse_method = respond_to?(parse_method, true) ? parse_method : :parse_text_value
136
+ send(parse_method, value)
137
+ end
77
138
 
78
- folded_line
79
- end
139
+ def parse_text_value(value)
140
+ value
141
+ end
80
142
 
143
+ def value_type
144
+ params ? params[0].value : default_value_type
81
145
  end
82
146
 
147
+ def default_value_type
148
+ "text"
149
+ end
150
+
151
+ def property_base_class
152
+ Vobject::Property
153
+ end
154
+
155
+ def parameter_base_class
156
+ Vobject::Parameter
157
+ end
158
+
159
+ def raise_invalid_initialization
160
+ raise "vObject property initialization failed"
161
+ end
83
162
  end
@@ -0,0 +1,46 @@
1
+ module Vobject
2
+ class PropertyValue
3
+ attr_accessor :value, :type, :errors, :norm
4
+
5
+ def <=>(another)
6
+ self.value <=> another.value
7
+ end
8
+
9
+ def initialize(val)
10
+ self.value = val
11
+ self.type = "text" # safe default
12
+ self.norm = nil
13
+ end
14
+
15
+ # raise_invalid_initialization if key != name
16
+
17
+ def to_s
18
+ value
19
+ end
20
+
21
+ def to_norm
22
+ if norm.nil?
23
+ norm = to_s
24
+ end
25
+ norm
26
+ end
27
+
28
+ def to_hash
29
+ value
30
+ end
31
+
32
+ def name
33
+ type
34
+ end
35
+
36
+ private
37
+
38
+ def default_value_type
39
+ "text"
40
+ end
41
+
42
+ def raise_invalid_initialization
43
+ raise "vObject property initialization failed"
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,106 @@
1
+ require "vobject"
2
+ require "vobject/property"
3
+ require "vobject/vcalendar/grammar"
4
+ require "json"
5
+
6
+ class Vobject::Component::Vcalendar < Vobject::Component
7
+ attr_accessor :comp_name, :children
8
+
9
+ class << self
10
+ def parse(vcf, strict)
11
+ hash = Vobject::Vcalendar::Grammar.new(strict).parse(vcf)
12
+ comp_name = hash.keys.first
13
+
14
+ new comp_name, hash[comp_name], hash[:errors]
15
+ end
16
+
17
+ def initialize(key, cs)
18
+ # super key, cs
19
+ self.comp_name = key
20
+ raise_invalid_initialization if key != name
21
+
22
+ self.children = []
23
+ if cs.is_a?(Array)
24
+ cs.each do |component|
25
+ c = []
26
+ component.each_key do |k|
27
+ val = component[k]
28
+ # iteration of array || hash values is making the value a key!
29
+ next if k.class == Array
30
+ next if k.class == Hash
31
+ cc = child_class(k, val)
32
+ c << cc.new(k, val)
33
+ end
34
+ children << c
35
+ end
36
+ else
37
+ cs.each_key do |k|
38
+ val = cs[k]
39
+ # iteration of array || hash values is making the value a key!
40
+ next if k.class == Array
41
+ next if k.class == Hash
42
+ cc = child_class(k, val)
43
+ children << cc.new(k, val)
44
+ end
45
+ end
46
+ end
47
+
48
+ def child_class(key, val)
49
+ base_class = if key == :VTODO
50
+ Vobject::Component::Vcalendar::ToDo
51
+ elsif key == :VFREEBUSY
52
+ Vobject::Component::Vcalendar::FreeBusy
53
+ elsif key == :JOURNAL
54
+ Vobject::Component::Vcalendar::Journal
55
+ elsif key == :STANDARD
56
+ Vobject::Component::Vcalendar::Timezone::Standard
57
+ elsif key == :DAYLIGHT
58
+ Vobject::Component::Vcalendar::Timezone::Daylight
59
+ elsif key == :VTIMEZONE
60
+ Vobject::Component::Vcalendar::Timezone
61
+ elsif key == :VEVENT
62
+ Vobject::Component::Vcalendar::Event
63
+ elsif key == :VALARM
64
+ Vobject::Component::Vcalendar::Alarm
65
+ elsif key == :VAVAILABILITY
66
+ Vobject::Component::Vcalendar::Vavailability
67
+ elsif key == :AVAILABLE
68
+ Vobject::Component::Vcalendar::Vavailability::Available
69
+ elsif !(val.is_a?(Hash) && !val.has_key?(:value))
70
+ property_base_class
71
+ else
72
+ Vobject::Component::Vcalendar
73
+ end
74
+ return base_class if [:CLASS, :OBJECT, :METHOD].include? key
75
+ camelized_key = key.to_s.downcase.split("_").map(&:capitalize).join("")
76
+ base_class.const_get(camelized_key) rescue base_class
77
+ end
78
+
79
+ private
80
+
81
+ def raise_invalid_parsing
82
+ raise "Vobject component parse failed"
83
+ end
84
+ end
85
+ end
86
+
87
+ class Vobject::Component::Vcalendar::ToDo < Vobject::Component::Vcalendar
88
+ end
89
+ class Vobject::Component::Vcalendar::Freebusy < Vobject::Component::Vcalendar
90
+ end
91
+ class Vobject::Component::Vcalendar::Journal < Vobject::Component::Vcalendar
92
+ end
93
+ class Vobject::Component::Vcalendar::Timezone < Vobject::Component::Vcalendar
94
+ end
95
+ class Vobject::Component::Vcalendar::Timezone::Standard < Vobject::Component::Vcalendar::Timezone
96
+ end
97
+ class Vobject::Component::Vcalendar::Timezone::Daylight < Vobject::Component::Vcalendar::Timezone
98
+ end
99
+ class Vobject::Component::Vcalendar::Event < Vobject::Component::Vcalendar
100
+ end
101
+ class Vobject::Component::Vcalendar::Alarm < Vobject::Component::Vcalendar
102
+ end
103
+ class Vobject::Component::Vcalendar::Vavailability < Vobject::Component::Vcalendar
104
+ end
105
+ class Vobject::Component::Vcalendar::Vavailability::Available < Vobject::Component::Vcalendar
106
+ end
@@ -0,0 +1,595 @@
1
+ require "rsec"
2
+ require "set"
3
+ require "uri"
4
+ require "date"
5
+ require "tzinfo"
6
+ include Rsec::Helpers
7
+ require_relative "../../c"
8
+ require_relative "../../error"
9
+ require "vobject"
10
+ require "vobject/vcalendar/typegrammars"
11
+ require "vobject/vcalendar/paramcheck"
12
+
13
+ module Vobject::Vcalendar
14
+ class Grammar
15
+ include C
16
+ attr_accessor :strict, :errors
17
+
18
+ class << self
19
+ # RFC 6868
20
+ def rfc6868decode(x)
21
+ x.gsub(/\^n/, "\n").gsub(/\^\^/, "^").gsub(/\^'/, '"')
22
+ end
23
+
24
+ def unfold(str)
25
+ str.gsub(/(\r|\n|\r\n)[ \t]/, "")
26
+ end
27
+ end
28
+
29
+ def vobject_grammar
30
+ # properties with value cardinality 1
31
+ @cardinality1 = {}
32
+ @cardinality1[:ICAL] = Set.new [:PRODID, :VERSION, :CALSCALE, :METHOD, :UID, :LAST_MOD, :URL,
33
+ :REFRESH_INTERVAL, :SOURCE, :COLOR]
34
+ @cardinality1[:EVENT] = Set.new [:UID, :DTSTAMP, :DTSTART, :CLASS, :CREATED, :DESCRIPTION, :GEO, :LAST_MOD,
35
+ :LOCATION, :ORGANIZER, :PRIORITY, :SEQ, :STATUS, :TRANSP, :URL, :RECURID, :COLOR]
36
+ @cardinality1[:TODO] = Set.new [:UID, :DTSTAMP, :CLASS, :COMPLETED, :CREATED, :DESCRIPTION, :DTSTART, :GEO, :LAST_MOD,
37
+ :LOCATION, :ORGANIZER, :PERCENT_COMPLETE, :PRIORITY, :SEQ, :STATUS, :SUMMARY, :URL, :RECURID, :COLOR]
38
+ @cardinality1[:JOURNAL] = Set.new [:UID, :DTSTAMP, :CLASS, :CREATED, :DTSTART, :LAST_MOD,
39
+ :ORGANIZER, :SEQ, :STATUS, :SUMMARY, :URL, :RECURID, :COLOR]
40
+ @cardinality1[:FREEBUSY] = Set.new [:UID, :DTSTAMP, :CONTACT, :DTSTART, :DTEND, :ORGANIZER, :URL]
41
+ @cardinality1[:TIMEZONE] = Set.new [:TZID, :LAST_MOD, :TZURL]
42
+ @cardinality1[:TZ] = Set.new [:DTSTART, :TZOFFSETTTO, :TZOFFSETFROM]
43
+ @cardinality1[:ALARM] = Set.new [:ACTION, :TRIGGER, :DURATION, :REPEAT, :DESCRIPTION, :SUMMARY]
44
+ @cardinality1[:VAVAILABILITY] = Set.new [:UID, :DTSTAMP, :DTSTART, :BUSYTYPE, :CLASS, :CREATED, :DESCRIPTION, :LAST_MOD,
45
+ :LOCATION, :ORGANIZER, :PRIORITY, :SEQ, :SUMMARY, :URL]
46
+ @cardinality1[:AVAILABLE] = Set.new [:DTSTAMP, :DTSTART, :UID, :CREATED, :DESCRIPTION, :LAST_MOD, :LOCATION,
47
+ :RECURID, :RRULE, :SUMMARY]
48
+ @cardinality1[:PARAM] = Set.new [:FMTTYPE, :LANGUAGE, :ALTREP, :FBTYPE, :TRANSP, :CUTYPE, :MEMBER, :ROLE, :PARTSTAT, :RSVP, :DELEGATED_TO,
49
+ :DELEGATED_FROM, :SENT_BY, :CN, :DIR, :RANGE, :RELTYPE, :RELATED, :DISPLAY, :FEATURE, :LABEL]
50
+
51
+ group = C::IANATOKEN
52
+ linegroup = group << "."
53
+ beginend = /BEGIN/i.r | /END/i.r
54
+
55
+ # parameters && parameter types
56
+ paramname = /ALTREP/i.r | /CN/i.r | /CUTYPE/i.r | /DELEGATED-FROM/i.r | /DELEGATED-TO/i.r |
57
+ /DIR/i.r | /ENCODING/i.r | /FMTTYPE/i.r | /FBTYPE/i.r | /LANGUAGE/i.r |
58
+ /MEMBER/i.r | /PARTSTAT/i.r | /RANGE/i.r | /RELATED/i.r | /RELTYPE/i.r |
59
+ /ROLE/i.r | /RSVP/i.r | /SENT-BY/i.r | /TZID/i.r | /RSCALE/i.r | /DISPLAY/i.r |
60
+ /FEATURE/i.r | /LABEL/i.r | /EMAIL/i.r
61
+ otherparamname = C::XNAME_VCAL | seq("".r ^ paramname, C::IANATOKEN)[1]
62
+ paramvalue = C::QUOTEDSTRING_VCAL.map { |s| self.class.rfc6868decode s } |
63
+ C::PTEXT_VCAL.map { |s| self.class.rfc6868decode(s) }
64
+ quotedparamvalue = C::QUOTEDSTRING_VCAL.map { |s| self.class.rfc6868decode s }
65
+ cutypevalue = /INDIVIDUAL/i.r | /GROUP/i.r | /RESOURCE/i.r | /ROOM/i.r | /UNKNOWN/i.r |
66
+ C::XNAME_VCAL | C::IANATOKEN.map
67
+ encodingvalue = /8BIT/i.r | /BASE64/i.r
68
+ fbtypevalue = /FREE/i.r | /BUSY/i.r | /BUSY-UNAVAILABLE/i.r | /BUSY-TENTATIVE/i.r |
69
+ C::XNAME_VCAL | C::IANATOKEN
70
+ partstatevent = /NEEDS-ACTION/i.r | /ACCEPTED/i.r | /DECLINED/i.r | /TENTATIVE/i.r |
71
+ /DELEGATED/i.r | C::XNAME_VCAL | C::IANATOKEN
72
+ partstattodo = /NEEDS-ACTION/i.r | /ACCEPTED/i.r | /DECLINED/i.r | /TENTATIVE/i.r |
73
+ /DELEGATED/i.r | /COMPLETED/i.r | /IN-PROCESS/i.r | C::XNAME_VCAL | C::IANATOKEN
74
+ partstatjour = /NEEDS-ACTION/i.r | /ACCEPTED/i.r | /DECLINED/i.r | C::XNAME_VCAL | C::IANATOKEN
75
+ partstatvalue = partstatevent | partstattodo | partstatjour
76
+ rangevalue = /THISANDFUTURE/i.r
77
+ relatedvalue = /START/i.r | /END/i.r
78
+ reltypevalue = /PARENT/i.r | /CHILD/i.r | /SIBLING/i.r | C::XNAME_VCAL | C::IANATOKEN
79
+ tzidvalue = seq("/".r._?, C::PTEXT_VCAL).map { |_, val| val }
80
+ valuetype = /BINARY/i.r | /BOOLEAN/i.r | /CAL-ADDRESS/i.r | /DATE-TIME/i.r | /DATE/i.r |
81
+ /DURATION/i.r | /FLOAT/i.r | /INTEGER/i.r | /PERIOD/i.r | /RECUR/i.r | /TEXT/i.r |
82
+ /TIME/i.r | /URI/i.r | /UTC-OFFSET/i.r | C::XNAME_VCAL | C::IANATOKEN
83
+ rolevalue = /CHAIR/i.r | /REQ-PARTICIPANT/i.r | /OPT-PARTICIPANT/i.r | /NON-PARTICIPANT/i.r |
84
+ C::XNAME_VCAL | C::IANATOKEN
85
+ pvalue_list = (seq(paramvalue << ",".r, lazy { pvalue_list }) & /[;:]/.r).map do |e, list|
86
+ [e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n"), list].flatten
87
+ end | (paramvalue & /[;:]/.r).map do |e|
88
+ [e.sub(Regexp.new("^\"(.+)\"$"), '\1').gsub(/\\n/, "\n")]
89
+ end
90
+ quoted_string_list = (seq(C::QUOTEDSTRING_VCAL << ",".r, lazy { quoted_string_list }) & /[;:]/.r).map do |e, list|
91
+ [self.class.rfc6868decode(e.sub(Regexp.new("^\"(.+)\"$"), "\1").gsub(/\\n/, "\n")), list].flatten
92
+ end | (C::QUOTEDSTRING_VCAL & /[;:]/.r).map do |e|
93
+ [self.class.rfc6868decode(e.sub(Regexp.new("^\"(.+)\"$"), "\1").gsub(/\\n/, "\n"))]
94
+ end
95
+
96
+ rfc4288regname = /[A-Za-z0-9!#$&.+^+-]{1,127}/.r
97
+ rfc4288typename = rfc4288regname
98
+ rfc4288subtypename = rfc4288regname
99
+ fmttypevalue = seq(rfc4288typename, "/", rfc4288subtypename).map(&:join)
100
+
101
+ # RFC 7986
102
+ displayval = /BADGE/i.r | /GRAPHIC/i.r | /FULLSIZE/i.r | /THUMBNAIL/i.r | C::XNAME_VCAL | C::IANATOKEN
103
+ displayvallist = seq(displayval << ",".r, lazy { displayvallist }) do |d, l|
104
+ [d, l].flatten
105
+ end | displayval.map { |d| [d] }
106
+ featureval = /AUDIO/i.r | /CHAT/i.r | /FEED/i.r | /MODERATOR/i.r | /PHONE/i.r | /SCREEN/i.r |
107
+ /VIDEO/i.r | C::XNAME_VCAL | C::IANATOKEN
108
+ featurevallist = seq(featureval << ",".r, lazy { featurevallist }) do |d, l|
109
+ [d, l].flatten
110
+ end | featureval.map { |d| [d] }
111
+
112
+ param = seq(/ALTREP/i.r, "=", quotedparamvalue) do |name, _, val|
113
+ { name.upcase.tr("-", "_").to_sym => val }
114
+ end | seq(/CN/i.r, "=", paramvalue) do |name, _, val|
115
+ { name.upcase.tr("-", "_").to_sym => val }
116
+ end | seq(/CUTYPE/i.r, "=", cutypevalue) do |name, _, val|
117
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
118
+ end | seq(/DELEGATED-FROM/i.r, "=", quoted_string_list) do |name, _, val|
119
+ val = val[0] if val.length == 1
120
+ { name.upcase.tr("-", "_").to_sym => val }
121
+ end | seq(/DELEGATED-TO/i.r, "=", quoted_string_list) do |name, _, val|
122
+ val = val[0] if val.length == 1
123
+ { name.upcase.tr("-", "_").to_sym => val }
124
+ end | seq(/DIR/i.r, "=", quotedparamvalue) do |name, _, val|
125
+ { name.upcase.tr("-", "_").to_sym => val }
126
+ end | seq(/ENCODING/i.r, "=", encodingvalue) do |name, _, val|
127
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
128
+ end | seq(/FMTTYPE/i.r, "=", fmttypevalue) do |name, _, val|
129
+ { name.upcase.tr("-", "_").to_sym => val.downcase }
130
+ end | seq(/FBTYPE/i.r, "=", fbtypevalue) do |name, _, val|
131
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
132
+ end | seq(/LANGUAGE/i.r, "=", C::RFC5646LANGVALUE) do |name, _, val|
133
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
134
+ end | seq(/MEMBER/i.r, "=", quoted_string_list) do |name, _, val|
135
+ val = val[0] if val.length == 1
136
+ { name.upcase.tr("-", "_").to_sym => val }
137
+ end | seq(/PARTSTAT/i.r, "=", partstatvalue) do |name, _, val|
138
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
139
+ end | seq(/RANGE/i.r, "=", rangevalue) do |name, _, val|
140
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
141
+ end | seq(/RELATED/i.r, "=", relatedvalue) do |name, _, val|
142
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
143
+ end | seq(/RELTYPE/i.r, "=", reltypevalue) do |name, _, val|
144
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
145
+ end | seq(/ROLE/i.r, "=", rolevalue) do |name, _, val|
146
+ { name.upcase.tr("-", "_").to_sym => val.upcase }
147
+ end | seq(/RSVP/i.r, "=", C::BOOLEAN) do |name, _, val|
148
+ { name.upcase.tr("-", "_").to_sym => val }
149
+ end | seq(/SENT-BY/i.r, "=", quotedparamvalue) do |name, _, val|
150
+ { name.upcase.tr("-", "_").to_sym => val }
151
+ end | seq(/TZID/i.r, "=", tzidvalue) do |name, _, val|
152
+ { name.upcase.tr("-", "_").to_sym => val }
153
+ end | seq(/VALUE/i.r, "=", valuetype) do |name, _, val|
154
+ { name.upcase.tr("-", "_").to_sym => val }
155
+ # RFC 7986
156
+ end | seq(/DISPLAY/i.r, "=", displayvallist) do |name, _, val|
157
+ { name.upcase.tr("-", "_").to_sym => val }
158
+ end | seq(/FEATURE/i.r, "=", featurevallist) do |name, _, val|
159
+ { name.upcase.tr("-", "_").to_sym => val }
160
+ end | seq(/EMAIL/i.r, "=", paramvalue) do |name, _, val|
161
+ { name.upcase.tr("-", "_").to_sym => val }
162
+ end | seq(/LABEL/i.r, "=", paramvalue) do |name, _, val|
163
+ { name.upcase.tr("-", "_").to_sym => val }
164
+ end | seq(otherparamname, "=", pvalue_list) do |name, _, val|
165
+ val = val[0] if val.length == 1
166
+ { name.upcase.tr("-", "_").to_sym => val }
167
+ end | seq(paramname, "=", pvalue_list) do |name, _, val|
168
+ parse_err("Violated format of parameter value #{name} = #{val}")
169
+ end
170
+
171
+ params = seq(";".r >> param & ";", lazy { params }) do |p, ps|
172
+ p.merge(ps) do |key, old, new|
173
+ if @cardinality1[:PARAM].include?(key)
174
+ parse_err("Violated cardinality of parameter #{key}")
175
+ end
176
+ [old, new].flatten
177
+ # deal with duplicate properties
178
+ end
179
+ end | seq(";".r >> param).map { |e| e[0] }
180
+
181
+ contentline = seq(linegroup._?, C::NAME_VCAL, params._? << ":".r,
182
+ C::VALUE, /(\r|\n|\r\n)/) do |g, name, p, value, _|
183
+ key = name.upcase.tr("-", "_").to_sym
184
+ hash = { key => { value: value } }
185
+ hash[key][:group] = g[0] unless g.empty?
186
+ errors << Paramcheck.paramcheck(strict, key, p.empty? ? {} : p[0], @ctx)
187
+ hash[key][:params] = p[0] unless p.empty?
188
+ hash
189
+ end
190
+
191
+ props = ("".r & beginend).map { {} } |
192
+ seq(contentline, lazy { props }) do |c, rest|
193
+ k = c.keys[0]
194
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :GENERIC, c[k][:value], @ctx)
195
+ errors << errors1
196
+ c.merge(rest) do |_, old, new|
197
+ [old, new].flatten
198
+ # deal with duplicate properties
199
+ end
200
+ end
201
+ alarmprops = ("".r & beginend).map { {} } |
202
+ seq(contentline, lazy { alarmprops }) do |c, rest|
203
+ k = c.keys[0]
204
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :ALARM, c[k][:value], @ctx)
205
+ errors << errors1
206
+ c.merge(rest) do |key, old, new|
207
+ if @cardinality1[:ALARM].include?(key.upcase)
208
+ parse_err("Violated cardinality of property #{key}")
209
+ end
210
+ [old, new].flatten
211
+ end
212
+ end
213
+ fbprops = ("".r & beginend).map { {} } |
214
+ seq(contentline, lazy { fbprops }) do |c, rest|
215
+ k = c.keys[0]
216
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :FREEBUSY, c[k][:value], @ctx)
217
+ errors << errors1
218
+ c.merge(rest) do |key, old, new|
219
+ if @cardinality1[:FREEBUSY].include?(key.upcase)
220
+ parse_err("Violated cardinality of property #{key}")
221
+ end
222
+ [old, new].flatten
223
+ end
224
+ end
225
+ journalprops = ("".r & beginend).map { {} } |
226
+ seq(contentline, lazy { journalprops }) do |c, rest|
227
+ k = c.keys[0]
228
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :JOURNAL, c[k][:value], @ctx)
229
+ errors << errors1
230
+ c.merge(rest) do |key, old, new|
231
+ if @cardinality1[:JOURNAL].include?(key.upcase)
232
+ parse_err("Violated cardinality of property #{key}")
233
+ end
234
+ [old, new].flatten
235
+ end
236
+ end
237
+ tzprops = ("".r & beginend).map { {} } |
238
+ seq(contentline, lazy { tzprops }) do |c, rest|
239
+ k = c.keys[0]
240
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :TZ, c[k][:value], @ctx)
241
+ errors << errors1
242
+ c.merge(rest) do |key, old, new|
243
+ if @cardinality1[:TZ].include?(key.upcase)
244
+ parse_err("Violated cardinality of property #{key}")
245
+ end
246
+ [old, new].flatten
247
+ end
248
+ end
249
+ standardc = seq(/BEGIN:STANDARD(\r|\n|\r\n)/i.r, tzprops, /END:STANDARD(\r|\n|\r\n)/i.r) do |_, e, _|
250
+ parse_err("Missing DTSTART property") unless e.has_key?(:DTSTART)
251
+ parse_err("Missing TZOFFSETTO property") unless e.has_key?(:TZOFFSETTO)
252
+ parse_err("Missing TZOFFSETFROM property") unless e.has_key?(:TZOFFSETFROM)
253
+ { STANDARD: { component: [e] } }
254
+ end
255
+ daylightc = seq(/BEGIN:DAYLIGHT(\r|\n|\r\n)/i.r, tzprops, /END:DAYLIGHT(\r|\n|\r\n)/i.r) do |_, e, _|
256
+ parse_err("Missing DTSTART property") unless e.has_key?(:DTSTART)
257
+ parse_err("Missing TZOFFSETTO property") unless e.has_key?(:TZOFFSETTO)
258
+ parse_err("Missing TZOFFSETFROM property") unless e.has_key?(:TZOFFSETFROM)
259
+ { DAYLIGHT: { component: [e] } }
260
+ end
261
+ timezoneprops =
262
+ seq(standardc, lazy { timezoneprops }) do |e, rest|
263
+ e.merge(rest) { |_, old, new| { component: [old[:component], new[:component]].flatten } }
264
+ end | seq(daylightc, lazy { timezoneprops }) do |e, rest|
265
+ e.merge(rest) { |_, old, new| { component: [old[:component], new[:component]].flatten } }
266
+ end | seq(contentline, lazy { timezoneprops }) do |e, rest|
267
+ k = e.keys[0]
268
+ e[k][:value], errors1 = Typegrammars.typematch(strict, k, e[k][:params], :TIMEZONE, e[k][:value], @ctx)
269
+ errors << errors1
270
+ e.merge(rest) do |key, old, new|
271
+ if @cardinality1[:TIMEZONE].include?(key.upcase)
272
+ parse_err("Violated cardinality of property #{key}")
273
+ end
274
+ [old, new].flatten
275
+ end
276
+ end |
277
+ ("".r & beginend).map { {} }
278
+ todoprops = ("".r & beginend).map { {} } |
279
+ seq(contentline, lazy { todoprops }) do |c, rest|
280
+ k = c.keys[0]
281
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :TODO, c[k][:value], @ctx)
282
+ errors << errors1
283
+ c.merge(rest) do |key, old, new|
284
+ if @cardinality1[:TODO].include?(key.upcase)
285
+ parse_err("Violated cardinality of property #{key}")
286
+ end
287
+ [old, new].flatten
288
+ end
289
+ end
290
+ eventprops = seq(contentline, lazy { eventprops }) do |c, rest|
291
+ k = c.keys[0]
292
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :EVENT, c[k][:value], @ctx)
293
+ errors << errors1
294
+ c.merge(rest) do |key, old, new|
295
+ if @cardinality1[:EVENT].include?(key.upcase)
296
+ parse_err("Violated cardinality of property #{key}")
297
+ end
298
+ [old, new].flatten
299
+ end
300
+ end |
301
+ ("".r & beginend).map { {} }
302
+ alarmc = seq(/BEGIN:VALARM(\r|\n|\r\n)/i.r, alarmprops, /END:VALARM(\r|\n|\r\n)/i.r) do |_, e, _|
303
+ parse_err("Missing ACTION property") unless e.has_key?(:ACTION)
304
+ parse_err("Missing TRIGGER property") unless e.has_key?(:TRIGGER)
305
+ if e.has_key?(:DURATION) && !e.has_key?(:REPEAT) || !e.has_key?(:DURATION) && e.has_key?(:REPEAT)
306
+ parse_err("Missing DURATION && REPEAT properties")
307
+ end
308
+ if e[:ACTION] == "AUDIO"
309
+ parse_err("Multiple ATTACH properties") if e.has_key?(:ATTACH) && e[:ATTACH].is_a?(Array)
310
+ parse_err("Invalid DESCRIPTION property") if e.has_key?(:DESCRIPTION)
311
+ parse_err("Invalid SUMMARY property") if e.has_key?(:SUMMARY)
312
+ parse_err("Invalid ATTENDEE property") if e.has_key?(:ATTENDEE)
313
+ elsif e[:ACTION] == "DISP"
314
+ parse_err("Missing DESCRIPTION property") unless e.has_key?(:DESCRIPTION)
315
+ parse_err("Invalid ATTACH property") if e.has_key?(:ATTACH)
316
+ parse_err("Invalid SUMMARY property") if e.has_key?(:SUMMARY)
317
+ parse_err("Invalid ATTENDEE property") if e.has_key?(:ATTENDEE)
318
+ elsif e[:ACTION] == "EMAIL"
319
+ parse_err("Missing DESCRIPTION property") unless e.has_key?(:DESCRIPTION)
320
+ end
321
+ { VALARM: { component: [e] } }
322
+ end
323
+ freebusyc = seq(/BEGIN:VFREEBUSY(\r|\n|\r\n)/i.r, fbprops, /END:VFREEBUSY(\r|\n|\r\n)/i.r) do |_, e, _|
324
+ parse_err("Missing DTSTAMP property") unless e.has_key?(:DTSTAMP)
325
+ parse_err("Missing UID property") unless e.has_key?(:UID)
326
+ parse_err("DTEND before DTSTART") if e.has_key?(:DTEND) && e.has_key?(:DTSTART) &&
327
+ e[:DTEND][:value] < e[:DTSTART][:value]
328
+ { VFREEBUSY: { component: [e] } }
329
+ end
330
+ journalc = seq(/BEGIN:VJOURNAL(\r|\n|\r\n)/i.r, journalprops, /END:VJOURNAL(\r|\n|\r\n)/i.r) do |_, e, _|
331
+ parse_err("Missing DTSTAMP property") unless e.has_key?(:DTSTAMP)
332
+ parse_err("Missing UID property") unless e.has_key?(:UID)
333
+ parse_err("Missing DTSTART property with RRULE property") if e.has_key?(:RRULE) && !e.has_key?(:DTSTART)
334
+ { VJOURNAL: { component: [e] } }
335
+ end
336
+ timezonec = seq(/BEGIN:VTIMEZONE(\r|\n|\r\n)/i.r, timezoneprops, /END:VTIMEZONE(\r|\n|\r\n)/i.r) do |_, e, _|
337
+ parse_err("Missing STANDARD || DAYLIGHT property") unless e.has_key?(:STANDARD) || e.has_key?(:DAYLIGHT)
338
+ { VTIMEZONE: { component: [e] } }
339
+ end
340
+ todoc = seq(/BEGIN:VTODO(\r|\n|\r\n)/i.r, todoprops, alarmc.star, /END:VTODO(\r|\n|\r\n)/i.r) do |_, e, a, _|
341
+ parse_err("Missing DTSTAMP property") unless e.has_key?(:DTSTAMP)
342
+ parse_err("Missing UID property") unless e.has_key?(:UID)
343
+ parse_err("Coocurring DUE && DURATION properties") if e.has_key?(:DUE) && e.has_key?(:DURATION)
344
+ parse_err("Missing DTSTART property with DURATION property") if e.has_key?(:DURATION) && !e.has_key?(:DTSTART)
345
+ parse_err("Missing DTSTART property with RRULE property") if e.has_key?(:RRULE) && !e.has_key?(:DTSTART)
346
+ parse_err("DUE before DTSTART") if e.has_key?(:DUE) &&
347
+ e.has_key?(:DTSTART) &&
348
+ e[:DUE][:value] < e[:DTSTART][:value]
349
+ # TODO not doing constraint that due && dtstart are both || neither local time
350
+ # TODO not doing constraint that recurrence-id && dtstart are both || neither local time
351
+ # TODO not doing constraint that recurrence-id && dtstart are both || neither date
352
+ a.each do |x|
353
+ e = e.merge(x) { |_, old, new| { component: [old[:component], new[:component]].flatten } }
354
+ end
355
+ { VTODO: { component: [e] } }
356
+ end
357
+ eventc = seq(/BEGIN:VEVENT(\r|\n|\r\n)/i.r, eventprops, alarmc.star, /END:VEVENT(\r|\n|\r\n)/i.r) do |_, e, a, _|
358
+ parse_err("Missing DTSTAMP property") unless e.has_key?(:DTSTAMP)
359
+ parse_err("Missing UID property") unless e.has_key?(:UID)
360
+ parse_err("Coocurring DTEND && DURATION properties") if e.has_key?(:DTEND) && e.has_key?(:DURATION)
361
+ parse_err("Missing DTSTART property with RRULE property") if e.has_key?(:RRULE) && !e.has_key?(:DTSTART)
362
+ parse_err("DTEND before DTSTART") if e.has_key?(:DTEND) &&
363
+ e.has_key?(:DTSTART) &&
364
+ e[:DTEND][:value] < e[:DTSTART][:value]
365
+ # TODO not doing constraint that dtend && dtstart are both || neither local time
366
+ a.each do |x|
367
+ e = e.merge(x) { |_, old, new| { component: [old[:component], new[:component]].flatten } }
368
+ end
369
+ { VEVENT: { component: [e] } }
370
+ end
371
+ xcomp = seq(/BEGIN:/i.r, C::XNAME_VCAL, /(\r|\n|\r\n)/i.r, props, /END:/i.r, C::XNAME_VCAL, /(\r|\n|\r\n)/i.r) do |_, n, _, p, _, n1, _|
372
+ n = n.upcase
373
+ n1 = n1.upcase
374
+ parse_err("Mismatch BEGIN:#{n}, END:#{n1}") if n != n1
375
+ { n1.to_sym => { component: [p] } }
376
+ end
377
+ ianacomp = seq(/BEGIN:/i.r ^ C::ICALPROPNAMES, C::IANATOKEN, /(\r|\n|\r\n)/i.r, props, /END:/i.r ^ C::ICALPROPNAMES, C::IANATOKEN, /(\r|\n|\r\n)/i.r) do |_, n, _, p, _, n1, _|
378
+ n = n.upcase
379
+ n1 = n1.upcase
380
+ parse_err("Mismatch BEGIN:#{n}, END:#{n1}") if n != n1
381
+ { n1.to_sym => { component: [p] } }
382
+ end
383
+ # RFC 7953
384
+ availableprops = seq(contentline, lazy { availableprops }) do |c, rest|
385
+ k = c.keys[0]
386
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :AVAILABLE, c[k][:value], @ctx)
387
+ errors << errors1
388
+ c.merge(rest) do |key, old, new|
389
+ if @cardinality1[:AVAILABLE].include?(key.upcase)
390
+ parse_err("Violated cardinality of property #{key}")
391
+ end
392
+ [old, new].flatten
393
+ end
394
+ end | ("".r & beginend).map { {} }
395
+ availablec = seq(/BEGIN:AVAILABLE(\r|\n|\r\n)/i.r, availableprops, /END:AVAILABLE(\r|\n|\r\n)/i.r) do |_, e, _|
396
+ # parse_err("Missing DTSTAMP property") unless e.has_key?(:DTSTAMP) # required in spec, but not in examples
397
+ parse_err("Missing DTSTART property") unless e.has_key?(:DTSTART)
398
+ parse_err("Missing UID property") unless e.has_key?(:UID)
399
+ parse_err("Coocurring DTEND && DURATION properties") if e.has_key?(:DTEND) && e.has_key?(:DURATION)
400
+ { AVAILABLE: { component: [e] } }
401
+ end
402
+ availabilityprops = seq(contentline, lazy { availabilityprops }) do |c, rest|
403
+ k = c.keys[0]
404
+ c[k][:value], errors1 = Typegrammars.typematch(strict, k, c[k][:params], :VAVAILABILITY, c[k][:value], @ctx)
405
+ errors << errors1
406
+ c.merge(rest) do |key, old, new|
407
+ if @cardinality1[:VAVAILABILITY].include?(key.upcase)
408
+ parse_err("Violated cardinality of property #{key}")
409
+ end
410
+ [old, new].flatten
411
+ end
412
+ end | ("".r & beginend).map { {} }
413
+ vavailabilityc = seq(/BEGIN:VAVAILABILITY(\r|\n|\r\n)/i.r, availabilityprops, availablec.star, /END:VAVAILABILITY(\r|\n|\r\n)/i.r) do |_, e, a, _|
414
+ parse_err("Missing DTSTAMP property") unless e.has_key?(:DTSTAMP)
415
+ parse_err("Missing UID property") unless e.has_key?(:UID)
416
+ parse_err("Coocurring DTEND && DURATION properties") if e.has_key?(:DTEND) && e.has_key?(:DURATION)
417
+ parse_err("Missing DTSTART property with DURATION property") if e.has_key?(:DURATION) && !e.has_key?(:DTSTART)
418
+ parse_err("DTEND before DTSTART") if e.has_key?(:DTEND) && e.has_key?(:DTSTART) && e[:DTEND][:value] < e[:DTSTART][:value]
419
+ # TODO not doing constraint that dtend && dtstart are both || neither local time
420
+ # TODO not doing constraint that each TZID param must have matching VTIMEZONE component
421
+ a.each do |x|
422
+ e = e.merge(x) { |_key, old, new| { component: [old[:component], new[:component]].flatten } }
423
+ end
424
+ { VAVAILABILITY: { component: [e] } }
425
+ end
426
+
427
+ component = eventc | todoc | journalc | freebusyc | timezonec | ianacomp | xcomp | vavailabilityc
428
+ components = seq(component, lazy { components }) do |c, r|
429
+ c.merge(r) do |_key, old, new|
430
+ { component: [old[:component], new[:component]].flatten }
431
+ end
432
+ end | component
433
+
434
+ calpropname = /CALSCALE/i.r | /METHOD/i.r | /PRODID/i.r | /VERSION/i.r |
435
+ /UID/i.r | /LAST-MOD/i.r | /URL/i.r | /REFRESH/i.r | /SOURCE/i.r | /COLOR/i.r | # RFC 7986
436
+ /NAME/i.r | /DESCRIPTION/i.r | /CATEGORIES/i.r | /IMAGE/i.r | # RFC 7986
437
+ C::XNAME_VCAL | C::IANATOKEN
438
+ calprop = seq(calpropname, params._? << ":".r, C::VALUE, /(\r|\n|\r\n)/) do |key, p, value, _|
439
+ key = key.upcase.tr("-", "_").to_sym
440
+ val, errors1 = Typegrammars.typematch(strict, key, p[0], :CALENDAR, value, @ctx)
441
+ errors << errors1
442
+ hash = { key => { value: val } }
443
+ errors << Paramcheck.paramcheck(strict, key, p.empty? ? {} : p[0], @ctx)
444
+ hash[key][:params] = p[0] unless p.empty?
445
+ hash
446
+ # TODO not doing constraint that each description must be in a different language
447
+ end
448
+ calprops = ("".r & beginend).map { {} } |
449
+ seq(calprop, lazy { calprops }) do |c, rest|
450
+ c.merge(rest) do |key, old, new|
451
+ if @cardinality1[:ICAL].include?(key.upcase)
452
+ parse_err("Violated cardinality of property #{key}")
453
+ end
454
+ [old, new].flatten
455
+ end
456
+ end
457
+ vobject = seq(/BEGIN:VCALENDAR(\r|\n|\r\n)/i.r, calprops, components, /END:VCALENDAR(\r|\n|\r\n)/i.r) do |_b, v, rest, _e|
458
+ parse_err("Missing PRODID attribute") unless v.has_key?(:PRODID)
459
+ parse_err("Missing VERSION attribute") unless v.has_key?(:VERSION)
460
+ rest.delete(:END)
461
+ if !v.has_key?(:METHOD) && rest.has_key?(:VEVENT)
462
+ rest[:VEVENT][:component].each do |e1|
463
+ parse_err("Missing DTSTART property from VEVENT component") if !e1.has_key?(:DTSTART)
464
+ end
465
+ end
466
+ tidyup(VCALENDAR: v.merge(rest), errors: errors.flatten)
467
+ end
468
+ vobject.eof
469
+ end
470
+
471
+ # any residual tidying of object
472
+ def tidyup(v)
473
+ # adjust any VTIMEZONE.{STANDARD|DAYLIGHT}.{DTSTART|RDATE} times from floating local to the time within the timezone component
474
+ if !v[:VCALENDAR].has_key?(:VTIMEZONE) || v[:VCALENDAR][:VTIMEZONE][:component].nil? || v[:VCALENDAR][:VTIMEZONE][:component].empty?
475
+ return v
476
+ elsif v[:VCALENDAR][:VTIMEZONE][:component].is_a?(Array)
477
+ v[:VCALENDAR][:VTIMEZONE][:component].map do |x|
478
+ timezoneadjust x
479
+ end
480
+ else
481
+ v[:VCALENDAR][:VTIMEZONE][:component] = timezoneadjust v[:VCALENDAR][:VTIMEZONE][:component]
482
+ end
483
+ v
484
+ end
485
+
486
+ def timezoneadjust(x)
487
+ if x[:TZID].nil? || x[:TZID].empty?
488
+ return x
489
+ end
490
+ # TODO deal with unregistered timezones
491
+ begin
492
+ tz = TZInfo::Timezone.get(x[:TZID][:value].value)
493
+ rescue
494
+ return x
495
+ end
496
+ [:STANDARD, :DAYLIGHT].each do |k|
497
+ next unless x.has_key?(k)
498
+ if x[k][:component].is_a?(Array)
499
+ x[k][:component].each do |y|
500
+ # subtracting an hour and a minute to avoid PeriodNotFound exceptions on the boundary between daylight saving && standard time
501
+ # if that doesn't work either, we'll rescue to floating localtime
502
+ # ... no, I will treat STANDARD times as standard, and DAYLIGHT times as daylight savings
503
+ # TODO lookup offsets applicable by parsing dates && offsets in the ical. I'd rather not.
504
+ begin
505
+ y[:DTSTART][:value].value[:time] = tz.local_to_utc(y[:DTSTART][:value].value[:time] - 3660, true) + 3660
506
+ rescue
507
+ # nop
508
+ else
509
+ y[:DTSTART][:value].value[:zone] = x[:TZID][:value].value
510
+ end
511
+ next unless y.has_key?(:RDATE)
512
+ if y[:RDATE].is_a?(Array)
513
+ y[:RDATE].each do |z|
514
+ z[:value].value.each do |w|
515
+ begin
516
+ w.value[:time] = tz.local_to_utc(w.value[:time] - 3660, true) + 3660
517
+ rescue
518
+ # nop
519
+ else
520
+ w.value[:zone] = x[:TZID][:value].value
521
+ end
522
+ end
523
+ end
524
+ else
525
+ begin
526
+ y[:RDATE][:value].value[:time] = tz.local_to_utc(y[:RDATE].value[:time] - 3660, true) + 3660
527
+ rescue
528
+ # nop
529
+ else
530
+ y[:RDATE][:value].value[:zone] = x[:TZID][:value].value
531
+ end
532
+ end
533
+ end
534
+ else
535
+ begin
536
+ x[k][:component][:DTSTART][:value].value[:time] = tz.local_to_utc(x[k][:component][:DTSTART][:value].value[:time] - 3660, true) + 3660
537
+ rescue
538
+ # nop
539
+ else
540
+ x[k][:component][:DTSTART][:value].value[:zone] = x[:TZID][:value].value
541
+ end
542
+ next unless x[k][:component].has_key?(:RDATE)
543
+ if x[k][:component][:RDATE].is_a?(Array)
544
+ x[k][:component][:RDATE].each do |z|
545
+ z[:value].value.each do |w|
546
+ begin
547
+ w.value[:time] = tz.local_to_utc(w.value[:time] - 3660, true) + 3660
548
+ rescue
549
+ # nop
550
+ else
551
+ w.value[:zone] = x[:TZID][:value].value
552
+ end
553
+ end
554
+ end
555
+ else
556
+ begin
557
+ x[k][:component][:RDATE][:value].value[:time] = tz.local_to_utc(x[k][:component][:RDATE][:value].value[:time] - 3660, true) + 3660
558
+ rescue
559
+ # nop
560
+ else
561
+ x[k][:component][:RDATE][:value].value[:zone] = x[:TZID][:value].value
562
+ end
563
+ end
564
+ end
565
+ end
566
+ x
567
+ end
568
+
569
+ def initialize(strict)
570
+ self.strict = strict
571
+ self.errors = []
572
+ end
573
+
574
+ def parse(vobject)
575
+ @ctx = Rsec::ParseContext.new self.class.unfold(vobject), "source"
576
+ ret = vobject_grammar._parse @ctx
577
+ if !ret || Rsec::INVALID[ret]
578
+ parse_err(@ctx.generate_error("source"))
579
+ ret = { VCALENDAR: nil, errors: errors.flatten }
580
+ end
581
+ Rsec::Fail.reset
582
+ ret
583
+ end
584
+
585
+ private
586
+
587
+ def parse_err(msg)
588
+ if strict
589
+ raise @ctx.report_error msg, "source"
590
+ else
591
+ errors << @ctx.report_error(msg, "source")
592
+ end
593
+ end
594
+ end
595
+ end