icalendar 1.5.4 → 2.0.0.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|