icalendar 1.5.4 → 2.0.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +1 -1
- data/.rspec +2 -0
- data/.travis.yml +1 -2
- data/History.txt +2 -7
- data/README.md +82 -107
- data/Rakefile +6 -7
- data/icalendar.gemspec +10 -9
- data/lib/icalendar.rb +17 -33
- data/lib/icalendar/alarm.rb +35 -0
- data/lib/icalendar/calendar.rb +17 -100
- data/lib/icalendar/component.rb +41 -403
- data/lib/icalendar/event.rb +51 -0
- data/lib/icalendar/freebusy.rb +27 -0
- data/lib/icalendar/has_components.rb +83 -0
- data/lib/icalendar/has_properties.rb +156 -0
- data/lib/icalendar/journal.rb +39 -0
- data/lib/icalendar/parser.rb +75 -403
- data/lib/icalendar/timezone.rb +53 -0
- data/lib/icalendar/todo.rb +52 -0
- data/lib/icalendar/tzinfo.rb +30 -30
- data/lib/icalendar/value.rb +80 -0
- data/lib/icalendar/values/array.rb +43 -0
- data/lib/icalendar/values/binary.rb +31 -0
- data/lib/icalendar/values/boolean.rb +17 -0
- data/lib/icalendar/values/cal_address.rb +8 -0
- data/lib/icalendar/values/date.rb +26 -0
- data/lib/icalendar/values/date_time.rb +34 -0
- data/lib/icalendar/values/duration.rb +48 -0
- data/lib/icalendar/values/float.rb +17 -0
- data/lib/icalendar/values/integer.rb +17 -0
- data/lib/icalendar/values/period.rb +46 -0
- data/lib/icalendar/values/recur.rb +63 -0
- data/lib/icalendar/values/text.rb +26 -0
- data/lib/icalendar/values/time.rb +34 -0
- data/lib/icalendar/values/time_with_zone.rb +31 -0
- data/lib/icalendar/values/uri.rb +19 -0
- data/lib/icalendar/values/utc_offset.rb +39 -0
- data/lib/icalendar/version.rb +5 -0
- data/spec/alarm_spec.rb +108 -0
- data/spec/calendar_spec.rb +167 -0
- data/spec/event_spec.rb +108 -0
- data/{test/fixtures/folding.ics → spec/fixtures/nondefault_values.ics} +2 -2
- data/{test → spec}/fixtures/single_event.ics +11 -14
- data/spec/fixtures/timezone.ics +35 -0
- data/spec/freebusy_spec.rb +7 -0
- data/spec/journal_spec.rb +7 -0
- data/spec/parser_spec.rb +26 -0
- data/spec/roundtrip_spec.rb +40 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/timezone_spec.rb +31 -0
- data/spec/todo_spec.rb +24 -0
- data/spec/tzinfo_spec.rb +85 -0
- data/spec/values/date_time_spec.rb +80 -0
- data/spec/values/duration_spec.rb +67 -0
- data/spec/values/period_spec.rb +47 -0
- data/spec/values/recur_spec.rb +47 -0
- data/spec/values/text_spec.rb +72 -0
- data/spec/values/utc_offset_spec.rb +41 -0
- metadata +129 -88
- data/GPL +0 -340
- data/examples/create_cal.rb +0 -45
- data/examples/parse_cal.rb +0 -20
- data/examples/single_event.ics +0 -18
- data/lib/hash_attrs.rb +0 -34
- data/lib/icalendar/base.rb +0 -47
- data/lib/icalendar/component/alarm.rb +0 -47
- data/lib/icalendar/component/event.rb +0 -131
- data/lib/icalendar/component/freebusy.rb +0 -38
- data/lib/icalendar/component/journal.rb +0 -60
- data/lib/icalendar/component/timezone.rb +0 -91
- data/lib/icalendar/component/todo.rb +0 -64
- data/lib/icalendar/conversions.rb +0 -107
- data/lib/icalendar/helpers.rb +0 -109
- data/lib/icalendar/parameter.rb +0 -33
- data/lib/icalendar/rrule.rb +0 -133
- data/lib/meta.rb +0 -32
- data/script/console +0 -10
- data/script/recur1.ics +0 -38
- data/script/tryit.rb +0 -13
- data/test/component/test_event.rb +0 -253
- data/test/component/test_timezone.rb +0 -74
- data/test/component/test_todo.rb +0 -31
- data/test/fixtures/life.ics +0 -46
- data/test/fixtures/nonstandard.ics +0 -25
- data/test/fixtures/simplecal.ics +0 -119
- data/test/interactive.rb +0 -17
- data/test/read_write.rb +0 -23
- data/test/test_calendar.rb +0 -167
- data/test/test_component.rb +0 -102
- data/test/test_conversions.rb +0 -104
- data/test/test_helper.rb +0 -7
- data/test/test_parameter.rb +0 -91
- data/test/test_parser.rb +0 -100
- data/test/test_tzinfo.rb +0 -83
- data/website/index.html +0 -70
- data/website/index.txt +0 -38
- data/website/javascripts/rounded_corners_lite.inc.js +0 -285
- data/website/stylesheets/screen.css +0 -159
- data/website/template.html.erb +0 -50
@@ -0,0 +1,51 @@
|
|
1
|
+
module Icalendar
|
2
|
+
|
3
|
+
class Event < Component
|
4
|
+
required_property :dtstamp, Icalendar::Values::DateTime
|
5
|
+
required_property :uid
|
6
|
+
# dtstart only required if calendar's method is nil
|
7
|
+
required_property :dtstart, Icalendar::Values::DateTime,
|
8
|
+
->(event, dtstart) { !dtstart.nil? || !(event.parent.nil? || event.parent.ip_method.nil?) }
|
9
|
+
|
10
|
+
optional_single_property :dtend, Icalendar::Values::DateTime
|
11
|
+
optional_single_property :duration, Icalendar::Values::Duration
|
12
|
+
mutually_exclusive_properties :dtend, :duration
|
13
|
+
|
14
|
+
optional_single_property :ip_class
|
15
|
+
optional_single_property :created, Icalendar::Values::DateTime
|
16
|
+
optional_single_property :description
|
17
|
+
optional_single_property :geo, Icalendar::Values::Float
|
18
|
+
optional_single_property :last_modified, Icalendar::Values::DateTime
|
19
|
+
optional_single_property :location
|
20
|
+
optional_single_property :organizer, Icalendar::Values::CalAddress
|
21
|
+
optional_single_property :priority, Icalendar::Values::Integer
|
22
|
+
optional_single_property :sequence, Icalendar::Values::Integer
|
23
|
+
optional_single_property :status
|
24
|
+
optional_single_property :summary
|
25
|
+
optional_single_property :transp
|
26
|
+
optional_single_property :url, Icalendar::Values::Uri
|
27
|
+
optional_single_property :recurrence_id, Icalendar::Values::DateTime
|
28
|
+
|
29
|
+
optional_property :rrule, Icalendar::Values::Recur, true
|
30
|
+
optional_property :attach, Icalendar::Values::Uri
|
31
|
+
optional_property :attendee, Icalendar::Values::CalAddress
|
32
|
+
optional_property :categories
|
33
|
+
optional_property :comment
|
34
|
+
optional_property :contact
|
35
|
+
optional_property :exdate, Icalendar::Values::DateTime
|
36
|
+
optional_property :request_status
|
37
|
+
optional_property :related_to
|
38
|
+
optional_property :resources
|
39
|
+
optional_property :rdate, Icalendar::Values::DateTime
|
40
|
+
|
41
|
+
component :alarm, false
|
42
|
+
|
43
|
+
def initialize
|
44
|
+
super 'event'
|
45
|
+
self.dtstamp = Icalendar::Values::DateTime.new Time.now.utc, 'tzid' => 'UTC'
|
46
|
+
self.uid = new_uid
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Icalendar
|
2
|
+
|
3
|
+
class Freebusy < Component
|
4
|
+
|
5
|
+
required_property :dtstamp, Icalendar::Values::DateTime
|
6
|
+
required_property :uid
|
7
|
+
|
8
|
+
optional_single_property :contact
|
9
|
+
optional_single_property :dtstart, Icalendar::Values::DateTime
|
10
|
+
optional_single_property :dtend, Icalendar::Values::DateTime
|
11
|
+
optional_single_property :organizer, Icalendar::Values::CalAddress
|
12
|
+
optional_single_property :url, Icalendar::Values::Uri
|
13
|
+
|
14
|
+
optional_property :attendee, Icalendar::Values::CalAddress
|
15
|
+
optional_property :comment
|
16
|
+
optional_property :freebusy, Icalendar::Values::Period
|
17
|
+
optional_property :request_status
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
super 'freebusy'
|
21
|
+
self.dtstamp = Icalendar::Values::DateTime.new Time.now.utc, 'tzid' => 'UTC'
|
22
|
+
self.uid = new_uid
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Icalendar
|
2
|
+
|
3
|
+
module HasComponents
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
base.class_eval do
|
8
|
+
attr_reader :custom_components
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
@custom_components = Hash.new { |h, k| h[k] = [] }
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def add_component(c)
|
18
|
+
c.parent = self
|
19
|
+
yield c if block_given?
|
20
|
+
send("#{c.name.downcase}s") << c
|
21
|
+
c
|
22
|
+
end
|
23
|
+
|
24
|
+
def method_missing(method, *args, &block)
|
25
|
+
method_name = method.to_s
|
26
|
+
if method_name =~ /^add_(x_\w+)$/
|
27
|
+
component_name = $1
|
28
|
+
custom = args.first || Component.new(component_name, component_name.upcase)
|
29
|
+
custom_components[component_name] << custom
|
30
|
+
yield custom if block_given?
|
31
|
+
custom
|
32
|
+
else
|
33
|
+
super
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def respond_to_missing?(method_name, include_private = false)
|
38
|
+
method_name.to_s.start_with?('add_x_') || super
|
39
|
+
end
|
40
|
+
|
41
|
+
module ClassMethods
|
42
|
+
def components
|
43
|
+
@components ||= []
|
44
|
+
end
|
45
|
+
|
46
|
+
def component(singular_name, find_by = :uid, klass = nil)
|
47
|
+
components = "#{singular_name}s"
|
48
|
+
self.components << components
|
49
|
+
component_var = "@#{components}"
|
50
|
+
|
51
|
+
define_method components do
|
52
|
+
if instance_variable_defined? component_var
|
53
|
+
instance_variable_get component_var
|
54
|
+
else
|
55
|
+
instance_variable_set component_var, []
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
define_method singular_name do |c = nil, &block|
|
60
|
+
if c.nil?
|
61
|
+
c = begin
|
62
|
+
klass ||= Icalendar.const_get singular_name.capitalize
|
63
|
+
klass.new
|
64
|
+
rescue NameError => ne
|
65
|
+
puts "WARN: #{ne.message}"
|
66
|
+
Component.new singular_name
|
67
|
+
end
|
68
|
+
end
|
69
|
+
add_component c, &block
|
70
|
+
end
|
71
|
+
|
72
|
+
define_method "find_#{singular_name}" do |id|
|
73
|
+
send(components).find { |c| c.send(find_by) == id }
|
74
|
+
end if find_by
|
75
|
+
|
76
|
+
define_method "add_#{singular_name}" do |c|
|
77
|
+
send singular_name, c
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
module Icalendar
|
2
|
+
|
3
|
+
module HasProperties
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
base.class_eval do
|
8
|
+
attr_reader :custom_properties
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(*args)
|
13
|
+
@custom_properties = Hash.new { |h, k| h[k] = [] }
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid?(strict = false)
|
18
|
+
self.class.required_properties.each_pair do |prop, validator|
|
19
|
+
validator.call(self, send(prop)) or return false
|
20
|
+
end
|
21
|
+
self.class.mutex_properties.each do |mutexprops|
|
22
|
+
mutexprops.map { |p| send p }.compact.size > 1 and return false
|
23
|
+
end
|
24
|
+
if strict
|
25
|
+
self.class.suggested_single_properties.each do |single_prop|
|
26
|
+
send(single_prop).size > 1 and return false
|
27
|
+
end
|
28
|
+
end
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(method, *args, &block)
|
33
|
+
method_name = method.to_s
|
34
|
+
if method_name.start_with? 'x_'
|
35
|
+
if method_name.end_with? '='
|
36
|
+
if args.first.is_a? Icalendar::Value
|
37
|
+
custom_properties[method_name.chomp('=')] << args.first
|
38
|
+
else
|
39
|
+
custom_properties[method_name.chomp('=')] << Icalendar::Values::Text.new(args.first)
|
40
|
+
end
|
41
|
+
else
|
42
|
+
custom_properties[method_name]
|
43
|
+
end
|
44
|
+
else
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def respond_to_missing?(method, include_private = false)
|
50
|
+
method.to_s.start_with?('x_') || super
|
51
|
+
end
|
52
|
+
|
53
|
+
module ClassMethods
|
54
|
+
def properties
|
55
|
+
single_properties + multiple_properties
|
56
|
+
end
|
57
|
+
|
58
|
+
def single_properties
|
59
|
+
@single_properties ||= []
|
60
|
+
end
|
61
|
+
|
62
|
+
def multiple_properties
|
63
|
+
@multiple_properties ||= []
|
64
|
+
end
|
65
|
+
|
66
|
+
def required_properties
|
67
|
+
@required_properties ||= {}
|
68
|
+
end
|
69
|
+
|
70
|
+
def suggested_single_properties
|
71
|
+
@suggested_single_properties ||= []
|
72
|
+
end
|
73
|
+
|
74
|
+
def mutex_properties
|
75
|
+
@mutex_properties ||= []
|
76
|
+
end
|
77
|
+
|
78
|
+
def default_property_types
|
79
|
+
@default_property_types ||= Hash.new { |h,k| Icalendar::Values::Text }
|
80
|
+
end
|
81
|
+
|
82
|
+
def required_property(prop, klass = Icalendar::Values::Text, validator = nil)
|
83
|
+
validator ||= ->(component, value) { !value.nil? }
|
84
|
+
self.required_properties[prop] = validator
|
85
|
+
single_property prop, klass
|
86
|
+
end
|
87
|
+
|
88
|
+
def required_multi_property(prop, klass = Icalendar::Values::Text, validator = nil)
|
89
|
+
validator ||= ->(component, value) { !value.compact.empty? }
|
90
|
+
self.required_properties[prop] = validator
|
91
|
+
multi_property prop, klass
|
92
|
+
end
|
93
|
+
|
94
|
+
def optional_single_property(prop, klass = Icalendar::Values::Text)
|
95
|
+
single_property prop, klass
|
96
|
+
end
|
97
|
+
|
98
|
+
def mutually_exclusive_properties(*properties)
|
99
|
+
self.mutex_properties << properties
|
100
|
+
end
|
101
|
+
|
102
|
+
def optional_property(prop, klass = Icalendar::Values::Text, suggested_single = false)
|
103
|
+
self.suggested_single_properties << prop if suggested_single
|
104
|
+
multi_property prop, klass
|
105
|
+
end
|
106
|
+
|
107
|
+
def single_property(prop, klass)
|
108
|
+
self.single_properties << prop.to_s
|
109
|
+
self.default_property_types[prop.to_s] = klass
|
110
|
+
|
111
|
+
define_method prop do
|
112
|
+
instance_variable_get "@#{prop}"
|
113
|
+
end
|
114
|
+
|
115
|
+
define_method "#{prop}=" do |value|
|
116
|
+
instance_variable_set "@#{prop}", map_property_value(value, klass, false)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def multi_property(prop, klass)
|
121
|
+
self.multiple_properties << prop.to_s
|
122
|
+
self.default_property_types[prop.to_s] = klass
|
123
|
+
property_var = "@#{prop}"
|
124
|
+
|
125
|
+
define_method "#{prop}=" do |value|
|
126
|
+
instance_variable_set property_var, [map_property_value(value, klass, true)].compact
|
127
|
+
end
|
128
|
+
|
129
|
+
define_method prop do
|
130
|
+
if instance_variable_defined? property_var
|
131
|
+
instance_variable_get property_var
|
132
|
+
else
|
133
|
+
send "#{prop}=", nil
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
define_method "append_#{prop}" do |value|
|
138
|
+
send(prop) << map_property_value(value, klass, true)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
def map_property_value(value, klass, multi_valued)
|
146
|
+
if value.nil? || value.is_a?(Icalendar::Value)
|
147
|
+
value
|
148
|
+
elsif value.is_a? ::Array
|
149
|
+
Icalendar::Values::Array.new value, klass, {}, {delimiter: (multi_valued ? ',' : ';')}
|
150
|
+
else
|
151
|
+
klass.new value
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Icalendar
|
2
|
+
|
3
|
+
class Journal < Component
|
4
|
+
|
5
|
+
required_property :dtstamp, Icalendar::Values::DateTime
|
6
|
+
required_property :uid
|
7
|
+
|
8
|
+
optional_single_property :ip_class
|
9
|
+
optional_single_property :created, Icalendar::Values::DateTime
|
10
|
+
optional_single_property :dtstart, Icalendar::Values::DateTime
|
11
|
+
optional_single_property :last_modified, Icalendar::Values::DateTime
|
12
|
+
optional_single_property :organizer, Icalendar::Values::CalAddress
|
13
|
+
optional_single_property :recurrence_id, Icalendar::Values::DateTime
|
14
|
+
optional_single_property :sequence, Icalendar::Values::Integer
|
15
|
+
optional_single_property :status
|
16
|
+
optional_single_property :summary
|
17
|
+
optional_single_property :url, Icalendar::Values::Uri
|
18
|
+
|
19
|
+
optional_property :rrule, Icalendar::Values::Recur, true
|
20
|
+
optional_property :attach, Icalendar::Values::Uri
|
21
|
+
optional_property :attendee, Icalendar::Values::CalAddress
|
22
|
+
optional_property :categories
|
23
|
+
optional_property :comment
|
24
|
+
optional_property :contact
|
25
|
+
optional_property :description
|
26
|
+
optional_property :exdate, Icalendar::Values::DateTime
|
27
|
+
optional_property :request_status
|
28
|
+
optional_property :related_to
|
29
|
+
optional_property :rdate, Icalendar::Values::DateTime
|
30
|
+
|
31
|
+
def initialize
|
32
|
+
super 'journal'
|
33
|
+
self.dtstamp = Icalendar::Values::DateTime.new Time.now.utc, 'tzid' => 'UTC'
|
34
|
+
self.uid = new_uid
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
data/lib/icalendar/parser.rb
CHANGED
@@ -1,441 +1,113 @@
|
|
1
|
-
=begin
|
2
|
-
Copyright (C) 2005 Jeff Rose
|
3
|
-
Copyright (C) 2005 Sam Roberts
|
4
|
-
|
5
|
-
This library is free software; you can redistribute it and/or modify it
|
6
|
-
under the same terms as the ruby language itself, see the file COPYING for
|
7
|
-
details.
|
8
|
-
=end
|
9
|
-
|
10
|
-
require 'date'
|
11
|
-
require 'uri'
|
12
|
-
require 'stringio'
|
13
|
-
|
14
1
|
module Icalendar
|
15
2
|
|
16
|
-
|
17
|
-
cals = Icalendar::Parser.new(src).parse
|
18
|
-
|
19
|
-
if single
|
20
|
-
cals.first
|
21
|
-
else
|
22
|
-
cals
|
23
|
-
end
|
24
|
-
end
|
25
|
-
|
26
|
-
class Parser < Icalendar::Base
|
27
|
-
# date = date-fullyear ["-"] date-month ["-"] date-mday
|
28
|
-
# date-fullyear = 4 DIGIT
|
29
|
-
# date-month = 2 DIGIT
|
30
|
-
# date-mday = 2 DIGIT
|
31
|
-
DATE = '(\d\d\d\d)-?(\d\d)-?(\d\d)'
|
32
|
-
|
33
|
-
# time = time-hour [":"] time-minute [":"] time-second [time-secfrac] [time-zone]
|
34
|
-
# time-hour = 2 DIGIT
|
35
|
-
# time-minute = 2 DIGIT
|
36
|
-
# time-second = 2 DIGIT
|
37
|
-
# time-secfrac = "," 1*DIGIT
|
38
|
-
# time-zone = "Z" / time-numzone
|
39
|
-
# time-numzome = sign time-hour [":"] time-minute
|
40
|
-
TIME = '(\d\d):?(\d\d):?(\d\d)(\.\d+)?(Z|[-+]\d\d:?\d\d)?'
|
41
|
-
|
42
|
-
# Defines if this is a strict parser.
|
43
|
-
attr_accessor :strict
|
44
|
-
|
45
|
-
def initialize(src, strict = true)
|
46
|
-
# Setup the parser method hash table
|
47
|
-
setup_parsers
|
48
|
-
|
49
|
-
# The default behavior is to raise an error when the parser
|
50
|
-
# finds an unknown property. Set this to false to discard
|
51
|
-
# unknown properties instead of raising an error.
|
52
|
-
@strict = strict
|
53
|
-
|
54
|
-
if src.respond_to?(:gets)
|
55
|
-
@file = src
|
56
|
-
elsif (not src.nil?) and src.respond_to?(:to_s)
|
57
|
-
@file = StringIO.new(src.to_s, 'r')
|
58
|
-
else
|
59
|
-
raise ArgumentError, "CalendarParser.new cannot be called with a #{src.class} type!"
|
60
|
-
end
|
61
|
-
|
62
|
-
@prev_line = @file.gets
|
63
|
-
@prev_line.chomp! unless @prev_line.nil?
|
3
|
+
class Parser
|
64
4
|
|
65
|
-
|
66
|
-
end
|
5
|
+
attr_reader :source, :strict
|
67
6
|
|
68
|
-
def
|
69
|
-
if
|
70
|
-
|
7
|
+
def initialize(source, strict = true)
|
8
|
+
if source.respond_to? :gets
|
9
|
+
@source = source
|
10
|
+
elsif source.respond_to? :to_s
|
11
|
+
@source = StringIO.new source.to_s, 'r'
|
71
12
|
else
|
72
|
-
|
73
|
-
if stripped =~ /\A#{Parser::PTEXT}\z/
|
74
|
-
stripped
|
75
|
-
else
|
76
|
-
%|"#{stripped}"|
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
# Define next line for an IO object.
|
82
|
-
# Works for strings now with StringIO
|
83
|
-
def next_line
|
84
|
-
line = @prev_line
|
85
|
-
|
86
|
-
if line.nil?
|
87
|
-
return nil
|
13
|
+
raise ArgumentError, 'Icalendar::Parser.new must be called with a String or IO object'
|
88
14
|
end
|
89
|
-
|
90
|
-
# Loop through until we get to a non-continuation line...
|
91
|
-
loop do
|
92
|
-
nextLine = @file.gets
|
93
|
-
@@logger.debug "new_line: #{nextLine}"
|
94
|
-
|
95
|
-
if !nextLine.nil?
|
96
|
-
nextLine.chomp!
|
97
|
-
end
|
98
|
-
|
99
|
-
# If it's a continuation line, add it to the last.
|
100
|
-
# If it's an empty line, drop it from the input.
|
101
|
-
if( nextLine =~ /^[ \t]/ )
|
102
|
-
line << nextLine[1, nextLine.size]
|
103
|
-
elsif( nextLine =~ /^$/ )
|
104
|
-
else
|
105
|
-
@prev_line = nextLine
|
106
|
-
break
|
107
|
-
end
|
108
|
-
end
|
109
|
-
line
|
15
|
+
@strict = strict
|
110
16
|
end
|
111
17
|
|
112
|
-
# Parse the calendar into an object representation
|
113
18
|
def parse
|
19
|
+
source.rewind
|
20
|
+
@data = source.gets and @data.chomp!
|
114
21
|
calendars = []
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
while (line = next_line)
|
119
|
-
fields = parse_line(line)
|
120
|
-
|
121
|
-
# Just iterate through until we find the beginning of a calendar object
|
122
|
-
if fields[:name] == "BEGIN" and fields[:value] == "VCALENDAR"
|
123
|
-
cal = parse_component Calendar.new
|
124
|
-
@@logger.debug "Added parsed calendar..."
|
125
|
-
calendars << cal
|
22
|
+
while (fields = next_fields)
|
23
|
+
if fields[:name] == 'begin' && fields[:value].downcase == 'vcalendar'
|
24
|
+
calendars << parse_component(Calendar.new)
|
126
25
|
end
|
127
26
|
end
|
128
|
-
|
129
27
|
calendars
|
130
28
|
end
|
131
29
|
|
132
30
|
private
|
133
31
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
# VFREEBUSY, VTIMEZONE
|
138
|
-
def parse_component(component = Calendar.new)
|
139
|
-
@@logger.debug "parsing new component..."
|
140
|
-
|
141
|
-
while (line = next_line)
|
142
|
-
fields = parse_line(line)
|
143
|
-
|
144
|
-
name = fields[:name].upcase
|
145
|
-
|
146
|
-
# Although properties are supposed to come before components, we should
|
147
|
-
# be able to handle them in any order...
|
148
|
-
if name == "END"
|
32
|
+
def parse_component(component)
|
33
|
+
while (fields = next_fields)
|
34
|
+
if fields[:name] == 'end'
|
149
35
|
break
|
150
|
-
elsif name ==
|
151
|
-
|
152
|
-
|
153
|
-
component.add_component parse_component(
|
154
|
-
|
155
|
-
component.add_component parse_component(
|
156
|
-
|
157
|
-
component.add_component parse_component(
|
158
|
-
when "VJOURNAL" # Journal entry
|
159
|
-
component.add_component parse_component(Journal.new)
|
160
|
-
when "VFREEBUSY" # Free/Busy section
|
161
|
-
component.add_component parse_component(Freebusy.new)
|
162
|
-
when "VTIMEZONE" # Timezone specification
|
163
|
-
component.add_component parse_component(Timezone.new)
|
164
|
-
when "STANDARD" # Standard time sub-component for timezone
|
165
|
-
component.add_component parse_component(Standard.new)
|
166
|
-
when "DAYLIGHT" # Daylight time sub-component for timezone
|
167
|
-
component.add_component parse_component(Daylight.new)
|
168
|
-
else # Uknown component type, skip to matching end
|
169
|
-
until ((line = next_line) == "END:#{fields[:value]}"); end
|
170
|
-
next
|
36
|
+
elsif fields[:name] == 'begin'
|
37
|
+
klass_name = fields[:value].gsub(/\AV/, '').downcase.capitalize
|
38
|
+
if Icalendar.const_defined? klass_name
|
39
|
+
component.add_component parse_component(Icalendar.const_get(klass_name).new)
|
40
|
+
elsif Icalendar::Timezone.const_defined? klass_name
|
41
|
+
component.add_component parse_component(Icalendar::Timezone.const_get(klass_name).new)
|
42
|
+
else
|
43
|
+
component.add_component parse_component(Component.new klass_name.downcase, fields[:value])
|
171
44
|
end
|
172
|
-
else
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
45
|
+
else
|
46
|
+
# new property
|
47
|
+
klass = component.class.default_property_types[fields[:name]]
|
48
|
+
if !fields[:params]['value'].nil?
|
49
|
+
klass_name = fields[:params].delete('value').first
|
50
|
+
unless klass_name.upcase == klass.value_type
|
51
|
+
klass_name = klass_name.downcase.gsub(/(?:\A|-)(.)/) { |m| m[-1].upcase }
|
52
|
+
klass = Icalendar::Values.const_get klass_name if Icalendar::Values.const_defined?(klass_name)
|
53
|
+
end
|
180
54
|
end
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
55
|
+
if klass.value_type != 'RECUR' && fields[:value] =~ /(?<!\\)([,;])/
|
56
|
+
delimiter = $1
|
57
|
+
prop_value = Icalendar::Values::Array.new fields[:value].split(/(?<!\\)[;,]/),
|
58
|
+
klass,
|
59
|
+
fields[:params],
|
60
|
+
delimiter: delimiter
|
61
|
+
else
|
62
|
+
prop_value = klass.new fields[:value], fields[:params]
|
187
63
|
end
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
if component.multiline_property?(name)
|
193
|
-
adder = "add_" + name
|
194
|
-
if component.respond_to?(adder)
|
195
|
-
component.send(adder, value, params)
|
196
|
-
else
|
197
|
-
raise(UnknownPropertyMethod, "Unknown property type: #{adder}") if strict
|
198
|
-
end
|
64
|
+
prop_name = %w(class method).include?(fields[:name]) ? "ip_#{fields[:name]}" : fields[:name]
|
65
|
+
if component.class.multiple_properties.include? prop_name
|
66
|
+
component.send "append_#{prop_name}", prop_value
|
199
67
|
else
|
200
|
-
|
201
|
-
component.send(name, value, params)
|
202
|
-
else
|
203
|
-
raise(UnknownPropertyMethod, "Unknown property type: #{name}") if strict
|
204
|
-
end
|
68
|
+
component.send "#{prop_name}=", prop_value
|
205
69
|
end
|
206
70
|
end
|
207
71
|
end
|
208
|
-
|
209
72
|
component
|
210
73
|
end
|
211
74
|
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
# *<Any character except CTLs, DQUOTE, ";", ":", ",">
|
222
|
-
PTEXT = '[^";:,]*'
|
223
|
-
|
224
|
-
# param-value = ptext / quoted-string
|
225
|
-
PVALUE = "#{QSTR}|#{PTEXT}"
|
226
|
-
|
227
|
-
# param = name "=" param-value *("," param-value)
|
228
|
-
PARAM = ";(#{NAME})(=?)((?:#{PVALUE})(?:,#{PVALUE})*)"
|
229
|
-
|
230
|
-
def parse_line(line)
|
231
|
-
unless line =~ %r{#{LINE}}i # Case insensitive match for a valid line
|
232
|
-
raise "Invalid line in calendar string!"
|
233
|
-
end
|
234
|
-
|
235
|
-
name = $1.upcase # The case insensitive part is upcased for easier comparison...
|
236
|
-
paramslist = $2
|
237
|
-
value = $3.gsub("\\;", ";").gsub("\\,", ",").gsub("\\n", "\n").gsub("\\\\", "\\")
|
238
|
-
|
239
|
-
# Parse the parameters
|
240
|
-
params = {}
|
241
|
-
if paramslist.size > 1
|
242
|
-
paramslist.scan( %r{#{PARAM}}i ) do
|
243
|
-
|
244
|
-
# parameter names are case-insensitive, and multi-valued
|
245
|
-
pname = $1
|
246
|
-
pvals = $3
|
247
|
-
|
248
|
-
# If there isn't an '=' sign then we need to do some custom
|
249
|
-
# business. Defaults to 'type'
|
250
|
-
if $2 == ""
|
251
|
-
pvals = $1
|
252
|
-
case $1
|
253
|
-
when /quoted-printable/i
|
254
|
-
pname = 'encoding'
|
255
|
-
|
256
|
-
when /base64/i
|
257
|
-
pname = 'encoding'
|
258
|
-
|
259
|
-
else
|
260
|
-
pname = 'type'
|
261
|
-
end
|
262
|
-
end
|
263
|
-
|
264
|
-
# Make entries into the params dictionary where the name
|
265
|
-
# is the key and the value is an array of values.
|
266
|
-
unless params.key? pname
|
267
|
-
params[pname] = []
|
268
|
-
end
|
269
|
-
|
270
|
-
# Save all the values into the array.
|
271
|
-
pvals.scan( %r{(#{PVALUE})} ) do
|
272
|
-
if $1.size > 0
|
273
|
-
params[pname] << $1
|
274
|
-
end
|
275
|
-
end
|
75
|
+
def next_fields
|
76
|
+
line = @data or return nil
|
77
|
+
loop do
|
78
|
+
@data = source.gets and @data.chomp!
|
79
|
+
if @data =~ /\A[ \t].+\z/
|
80
|
+
line << @data[1, @data.size]
|
81
|
+
elsif @data !~ /\A\s*\z/
|
82
|
+
break
|
276
83
|
end
|
277
84
|
end
|
278
|
-
|
279
|
-
{:name => name, :value => value, :params => params}
|
280
|
-
end
|
281
|
-
|
282
|
-
## Following is a collection of parsing functions for various
|
283
|
-
## icalendar property value data types... First we setup
|
284
|
-
## a hash with property names pointing to methods...
|
285
|
-
def setup_parsers
|
286
|
-
@parsers = {}
|
287
|
-
|
288
|
-
# Integer properties
|
289
|
-
m = self.method(:parse_integer)
|
290
|
-
@parsers["PERCENT-COMPLETE"] = m
|
291
|
-
@parsers["PRIORITY"] = m
|
292
|
-
@parsers["REPEAT"] = m
|
293
|
-
@parsers["SEQUENCE"] = m
|
294
|
-
|
295
|
-
# Dates and Times
|
296
|
-
m = self.method(:parse_datetime)
|
297
|
-
@parsers["COMPLETED"] = m
|
298
|
-
@parsers["DTEND"] = m
|
299
|
-
@parsers["DUE"] = m
|
300
|
-
@parsers["DTSTART"] = m
|
301
|
-
@parsers["RECURRENCE-ID"] = m
|
302
|
-
@parsers["CREATED"] = m
|
303
|
-
@parsers["DTSTAMP"] = m
|
304
|
-
@parsers["LAST-MODIFIED"] = m
|
305
|
-
@parsers["ACKNOWLEDGED"] = m
|
306
|
-
|
307
|
-
m = self.method(:parse_multi_datetime)
|
308
|
-
@parsers["EXDATE"] = m
|
309
|
-
@parsers["RDATE"] = m
|
310
|
-
|
311
|
-
# URI's
|
312
|
-
m = self.method(:parse_uri)
|
313
|
-
@parsers["TZURL"] = m
|
314
|
-
@parsers["ATTENDEE"] = m
|
315
|
-
@parsers["ORGANIZER"] = m
|
316
|
-
@parsers["URL"] = m
|
317
|
-
|
318
|
-
# This is a URI by default, and if its not a valid URI
|
319
|
-
# it will be returned as a string which works for binary data
|
320
|
-
# the other possible type.
|
321
|
-
@parsers["ATTACH"] = m
|
322
|
-
|
323
|
-
# GEO
|
324
|
-
m = self.method(:parse_geo)
|
325
|
-
@parsers["GEO"] = m
|
326
|
-
|
327
|
-
#RECUR
|
328
|
-
m = self.method(:parse_recur)
|
329
|
-
@parsers["RRULE"] = m
|
330
|
-
@parsers["EXRULE"] = m
|
331
|
-
|
332
|
-
end
|
333
|
-
|
334
|
-
# Booleans
|
335
|
-
# NOTE: It appears that although this is a valid data type
|
336
|
-
# there aren't any properties that use it... Maybe get
|
337
|
-
# rid of this in the future.
|
338
|
-
def parse_boolean(name, params, value)
|
339
|
-
if value.upcase == "FALSE"
|
340
|
-
false
|
341
|
-
else
|
342
|
-
true
|
343
|
-
end
|
85
|
+
parse_fields line
|
344
86
|
end
|
345
87
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
if /Z$/ =~ value
|
354
|
-
timezone = "UTC"
|
355
|
-
else
|
356
|
-
timezone = params["TZID"].first if params["TZID"]
|
357
|
-
end
|
358
|
-
result.icalendar_tzid = timezone
|
359
|
-
end
|
360
|
-
result
|
361
|
-
rescue Exception
|
362
|
-
value
|
363
|
-
end
|
88
|
+
NAME = '[-a-zA-Z0-9]+'
|
89
|
+
QSTR = '"[^"]*"'
|
90
|
+
PTEXT = '[^";:,]*'
|
91
|
+
PVALUE = "(?:#{QSTR}|#{PTEXT})"
|
92
|
+
PARAM = "(#{NAME})=(#{PVALUE}(?:,#{PVALUE})*)"
|
93
|
+
VALUE = '.*'
|
94
|
+
LINE = "(?<name>#{NAME})(?<params>(?:;#{PARAM})*):(?<value>#{VALUE})"
|
364
95
|
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
else
|
374
|
-
dt = DateTime.parse val
|
375
|
-
if /Z$/ =~ val
|
376
|
-
timezone = 'UTC'
|
377
|
-
else
|
378
|
-
timezone = tzid
|
379
|
-
end
|
380
|
-
dt.icalendar_tzid = timezone
|
381
|
-
result << dt
|
96
|
+
def parse_fields(input)
|
97
|
+
parts = %r{#{LINE}}.match(input) or raise "Invalid iCalendar input line: #{input}"
|
98
|
+
params = {}
|
99
|
+
parts[:params].scan %r{#{PARAM}} do |match|
|
100
|
+
param_name = match[0].downcase
|
101
|
+
params[param_name] ||= []
|
102
|
+
match[1].scan %r{#{PVALUE}} do |param_value|
|
103
|
+
params[param_name] << param_value.gsub(/\A"|"\z/, '') if param_value.size > 0
|
382
104
|
end
|
383
105
|
end
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
def parse_recur(name, params, value)
|
390
|
-
[::Icalendar::RRule.new(name, params, value)]
|
391
|
-
end
|
392
|
-
|
393
|
-
# Durations
|
394
|
-
# TODO: Need to figure out the best way to represent durations
|
395
|
-
# so just returning string for now.
|
396
|
-
def parse_duration(name, params, value)
|
397
|
-
value
|
106
|
+
{
|
107
|
+
name: parts[:name].downcase.gsub('-', '_'),
|
108
|
+
params: params,
|
109
|
+
value: parts[:value]
|
110
|
+
}
|
398
111
|
end
|
399
|
-
|
400
|
-
# Floats
|
401
|
-
# NOTE: returns 0.0 if it can't parse the value
|
402
|
-
def parse_float(name, params, value)
|
403
|
-
value.to_f
|
404
|
-
end
|
405
|
-
|
406
|
-
# Integers
|
407
|
-
# NOTE: returns 0 if it can't parse the value
|
408
|
-
def parse_integer(name, params, value)
|
409
|
-
value.to_i
|
410
|
-
end
|
411
|
-
|
412
|
-
# Periods
|
413
|
-
# TODO: Got to figure out how to represent periods also...
|
414
|
-
def parse_period(name, params, value)
|
415
|
-
value
|
416
|
-
end
|
417
|
-
|
418
|
-
# Calendar Address's & URI's
|
419
|
-
# NOTE: invalid URI's will be returned as strings...
|
420
|
-
def parse_uri(name, params, value)
|
421
|
-
begin
|
422
|
-
URI.parse(value)
|
423
|
-
rescue Exception
|
424
|
-
value
|
425
|
-
end
|
426
|
-
end
|
427
|
-
|
428
|
-
# Geographical location (GEO)
|
429
|
-
# NOTE: returns an array with two floats (long & lat)
|
430
|
-
# if the parsing fails return the string
|
431
|
-
def parse_geo(name, params, value)
|
432
|
-
strloc = value.split(';')
|
433
|
-
if strloc.size != 2
|
434
|
-
return value
|
435
|
-
end
|
436
|
-
|
437
|
-
Geo.new(strloc[0].to_f, strloc[1].to_f)
|
438
|
-
end
|
439
|
-
|
440
112
|
end
|
441
113
|
end
|