vobject 0.1.0 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
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