curzonj-icalendar 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +56 -0
- data/GPL +340 -0
- data/README +266 -0
- data/Rakefile +110 -0
- data/docs/rfcs/itip_notes.txt +69 -0
- data/docs/rfcs/rfc2425.pdf +0 -0
- data/docs/rfcs/rfc2426.pdf +0 -0
- data/docs/rfcs/rfc2445.pdf +0 -0
- data/docs/rfcs/rfc2446.pdf +0 -0
- data/docs/rfcs/rfc2447.pdf +0 -0
- data/docs/rfcs/rfc3283.txt +738 -0
- data/examples/create_cal.rb +45 -0
- data/examples/parse_cal.rb +20 -0
- data/examples/single_event.ics +18 -0
- data/lib/hash_attrs.rb +34 -0
- data/lib/icalendar.rb +39 -0
- data/lib/icalendar/base.rb +43 -0
- data/lib/icalendar/calendar.rb +113 -0
- data/lib/icalendar/component.rb +442 -0
- data/lib/icalendar/component/alarm.rb +44 -0
- data/lib/icalendar/component/event.rb +129 -0
- data/lib/icalendar/component/freebusy.rb +38 -0
- data/lib/icalendar/component/journal.rb +61 -0
- data/lib/icalendar/component/timezone.rb +105 -0
- data/lib/icalendar/component/todo.rb +64 -0
- data/lib/icalendar/conversions.rb +150 -0
- data/lib/icalendar/helpers.rb +109 -0
- data/lib/icalendar/parameter.rb +33 -0
- data/lib/icalendar/parser.rb +396 -0
- data/lib/meta.rb +32 -0
- data/test/calendar_test.rb +71 -0
- data/test/component/event_test.rb +256 -0
- data/test/component/todo_test.rb +13 -0
- data/test/component_test.rb +76 -0
- data/test/conversions_test.rb +97 -0
- data/test/fixtures/folding.ics +23 -0
- data/test/fixtures/life.ics +46 -0
- data/test/fixtures/simplecal.ics +119 -0
- data/test/fixtures/single_event.ics +23 -0
- data/test/interactive.rb +17 -0
- data/test/parameter_test.rb +29 -0
- data/test/parser_test.rb +84 -0
- data/test/read_write.rb +23 -0
- metadata +105 -0
@@ -0,0 +1,150 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright (C) 2005 Jeff Rose
|
3
|
+
|
4
|
+
This library is free software; you can redistribute it and/or modify it
|
5
|
+
under the same terms as the ruby language itself, see the file COPYING for
|
6
|
+
details.
|
7
|
+
=end
|
8
|
+
|
9
|
+
require 'date'
|
10
|
+
|
11
|
+
### Add some to_ical methods to classes
|
12
|
+
|
13
|
+
# class Object
|
14
|
+
# def to_ical
|
15
|
+
# raise(NotImplementedError, "This object does not implement the to_ical method!")
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
|
19
|
+
module Icalendar
|
20
|
+
module TzidSupport
|
21
|
+
attr_accessor :icalendar_tzid
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
require 'uri/generic'
|
26
|
+
|
27
|
+
class String
|
28
|
+
def to_ical
|
29
|
+
self
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class Fixnum
|
34
|
+
def to_ical
|
35
|
+
"#{self}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Bignum
|
40
|
+
def to_ical
|
41
|
+
"#{self}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class Float
|
46
|
+
def to_ical
|
47
|
+
"#{self}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# From the spec: "Values in a list of values MUST be separated by a COMMA
|
52
|
+
# character (US-ASCII decimal 44)."
|
53
|
+
class Array
|
54
|
+
def to_ical
|
55
|
+
map{|elem| elem.to_ical}.join ','
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module URI
|
60
|
+
class Generic
|
61
|
+
def to_ical
|
62
|
+
"#{self}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class DateTime < Date
|
68
|
+
attr_accessor :ical_params
|
69
|
+
include Icalendar::TzidSupport
|
70
|
+
|
71
|
+
def to_ical
|
72
|
+
s = ""
|
73
|
+
|
74
|
+
# 4 digit year
|
75
|
+
s << self.year.to_s
|
76
|
+
|
77
|
+
# Double digit month
|
78
|
+
s << "0" unless self.month > 9
|
79
|
+
s << self.month.to_s
|
80
|
+
|
81
|
+
# Double digit day
|
82
|
+
s << "0" unless self.day > 9
|
83
|
+
s << self.day.to_s
|
84
|
+
|
85
|
+
s << "T"
|
86
|
+
|
87
|
+
# Double digit hour
|
88
|
+
s << "0" unless self.hour > 9
|
89
|
+
s << self.hour.to_s
|
90
|
+
|
91
|
+
# Double digit minute
|
92
|
+
s << "0" unless self.min > 9
|
93
|
+
s << self.min.to_s
|
94
|
+
|
95
|
+
# Double digit second
|
96
|
+
s << "0" unless self.sec > 9
|
97
|
+
s << self.sec.to_s
|
98
|
+
|
99
|
+
# UTC time gets a Z suffix
|
100
|
+
if icalendar_tzid == "UTC"
|
101
|
+
s << "Z"
|
102
|
+
end
|
103
|
+
|
104
|
+
s
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class Date
|
109
|
+
attr_accessor :ical_params
|
110
|
+
def to_ical(utc = false)
|
111
|
+
s = ""
|
112
|
+
|
113
|
+
# 4 digit year
|
114
|
+
s << self.year.to_s
|
115
|
+
|
116
|
+
# Double digit month
|
117
|
+
s << "0" unless self.month > 9
|
118
|
+
s << self.month.to_s
|
119
|
+
|
120
|
+
# Double digit day
|
121
|
+
s << "0" unless self.day > 9
|
122
|
+
s << self.day.to_s
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class Time
|
127
|
+
attr_accessor :ical_params
|
128
|
+
def to_ical(utc = false)
|
129
|
+
s = ""
|
130
|
+
|
131
|
+
# Double digit hour
|
132
|
+
s << "0" unless self.hour > 9
|
133
|
+
s << self.hour.to_s
|
134
|
+
|
135
|
+
# Double digit minute
|
136
|
+
s << "0" unless self.min > 9
|
137
|
+
s << self.min.to_s
|
138
|
+
|
139
|
+
# Double digit second
|
140
|
+
s << "0" unless self.sec > 9
|
141
|
+
s << self.sec.to_s
|
142
|
+
|
143
|
+
# UTC time gets a Z suffix
|
144
|
+
if utc
|
145
|
+
s << "Z"
|
146
|
+
end
|
147
|
+
|
148
|
+
s
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,109 @@
|
|
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
|
+
module Icalendar
|
11
|
+
module DateProp
|
12
|
+
# date = date-fullyear date-month date-mday
|
13
|
+
# date-fullyear = 4 DIGIT
|
14
|
+
# date-month = 2 DIGIT
|
15
|
+
# date-mday = 2 DIGIT
|
16
|
+
DATE = '(\d\d\d\d)(\d\d)(\d\d)'
|
17
|
+
|
18
|
+
# time = time-hour [":"] time-minute [":"] time-second [time-secfrac] [time-zone]
|
19
|
+
# time-hour = 2 DIGIT
|
20
|
+
# time-minute = 2 DIGIT
|
21
|
+
# time-second = 2 DIGIT
|
22
|
+
# time-secfrac = "," 1*DIGIT
|
23
|
+
# time-zone = "Z" / time-numzone
|
24
|
+
# time-numzome = sign time-hour [":"] time-minute
|
25
|
+
# TIME = '(\d\d)(\d\d)(\d\d)(Z)?'
|
26
|
+
TIME = '(\d\d)(\d\d)(\d\d)'
|
27
|
+
|
28
|
+
# This method is called automatically when the module is mixed in.
|
29
|
+
# I guess you have to do this to mixin class methods rather than instance methods.
|
30
|
+
def self.append_features(base)
|
31
|
+
super
|
32
|
+
klass.extend(ClassMethods)
|
33
|
+
end
|
34
|
+
|
35
|
+
# This is made a sub-module just so it can be added as class
|
36
|
+
# methods rather than instance methods.
|
37
|
+
module ClassMethods
|
38
|
+
def date_property(dp, alias_name = nil)
|
39
|
+
dp = "#{dp}".strip.downcase
|
40
|
+
getter = dp
|
41
|
+
setter = "#{dp}="
|
42
|
+
query = "#{dp}?"
|
43
|
+
|
44
|
+
unless instance_methods.include? getter
|
45
|
+
code = <<-code
|
46
|
+
def #{getter}(*a)
|
47
|
+
if a.empty?
|
48
|
+
@properties[#{dp.upcase}]
|
49
|
+
else
|
50
|
+
self.#{dp} = a.first
|
51
|
+
end
|
52
|
+
end
|
53
|
+
code
|
54
|
+
|
55
|
+
module_eval code
|
56
|
+
end
|
57
|
+
|
58
|
+
unless instance_methods.include? setter
|
59
|
+
code = <<-code
|
60
|
+
def #{setter} a
|
61
|
+
@properties[#{dp.upcase}] = a
|
62
|
+
end
|
63
|
+
code
|
64
|
+
|
65
|
+
module_eval code
|
66
|
+
end
|
67
|
+
|
68
|
+
unless instance_methods.include? query
|
69
|
+
code = <<-code
|
70
|
+
def #{query}
|
71
|
+
@properties.has_key?(#{dp.upcase})
|
72
|
+
end
|
73
|
+
code
|
74
|
+
|
75
|
+
module_eval code
|
76
|
+
end
|
77
|
+
|
78
|
+
# Define the getter
|
79
|
+
getter = "get#{property.to_s.capitalize}"
|
80
|
+
define_method(getter.to_sym) do
|
81
|
+
puts "inside getting..."
|
82
|
+
getDateProperty(property.to_s.upcase)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Define the setter
|
86
|
+
setter = "set#{property.to_s.capitalize}"
|
87
|
+
define_method(setter.to_sym) do |*params|
|
88
|
+
date = params[0]
|
89
|
+
utc = params[1]
|
90
|
+
puts "inside setting..."
|
91
|
+
setDateProperty(property.to_s.upcase, date, utc)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Create aliases if a name was specified
|
95
|
+
# if not aliasName.nil?
|
96
|
+
# gasym = "get#{aliasName.to_s.capitalize}".to_sym
|
97
|
+
# gsym = getter.to_sym
|
98
|
+
# alias gasym gsym
|
99
|
+
|
100
|
+
# sasym = "set#{aliasName.to_s.capitalize}".to_sym
|
101
|
+
# ssym = setter.to_sym
|
102
|
+
# alias sasym ssym
|
103
|
+
# end
|
104
|
+
end
|
105
|
+
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright (C) 2005 Jeff Rose
|
3
|
+
|
4
|
+
This library is free software; you can redistribute it and/or modify it
|
5
|
+
under the same terms as the ruby language itself, see the file COPYING for
|
6
|
+
details.
|
7
|
+
=end
|
8
|
+
|
9
|
+
module Icalendar
|
10
|
+
|
11
|
+
# A property can have attributes associated with it. These "property
|
12
|
+
# parameters" contain meta-information about the property or the
|
13
|
+
# property value. Property parameters are provided to specify such
|
14
|
+
# information as the location of an alternate text representation for a
|
15
|
+
# property value, the language of a text property value, the data type
|
16
|
+
# of the property value and other attributes.
|
17
|
+
class Parameter < Icalendar::Content
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
s = ""
|
21
|
+
|
22
|
+
s << "#{@name}="
|
23
|
+
if is_escapable?
|
24
|
+
s << escape(print_value())
|
25
|
+
else
|
26
|
+
s << print_value
|
27
|
+
end
|
28
|
+
|
29
|
+
s
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,396 @@
|
|
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
|
+
module Icalendar
|
15
|
+
|
16
|
+
def Icalendar.parse(src, single = false)
|
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
|
+
def initialize(src)
|
43
|
+
# Setup the parser method hash table
|
44
|
+
setup_parsers()
|
45
|
+
|
46
|
+
if src.respond_to?(:gets)
|
47
|
+
@file = src
|
48
|
+
elsif (not src.nil?) and src.respond_to?(:to_s)
|
49
|
+
@file = StringIO.new(src.to_s, 'r')
|
50
|
+
else
|
51
|
+
raise ArgumentError, "CalendarParser.new cannot be called with a #{src.class} type!"
|
52
|
+
end
|
53
|
+
|
54
|
+
@prev_line = @file.gets
|
55
|
+
@prev_line.chomp! unless @prev_line.nil?
|
56
|
+
|
57
|
+
@@logger.debug("New Calendar Parser: #{@file.inspect}")
|
58
|
+
end
|
59
|
+
|
60
|
+
# Define next line for an IO object.
|
61
|
+
# Works for strings now with StringIO
|
62
|
+
def next_line
|
63
|
+
line = @prev_line
|
64
|
+
|
65
|
+
if line.nil?
|
66
|
+
return nil
|
67
|
+
end
|
68
|
+
|
69
|
+
# Loop through until we get to a non-continuation line...
|
70
|
+
loop do
|
71
|
+
nextLine = @file.gets
|
72
|
+
@@logger.debug "new_line: #{nextLine}"
|
73
|
+
|
74
|
+
if !nextLine.nil?
|
75
|
+
nextLine.chomp!
|
76
|
+
end
|
77
|
+
|
78
|
+
# If it's a continuation line, add it to the last.
|
79
|
+
# If it's an empty line, drop it from the input.
|
80
|
+
if( nextLine =~ /^[ \t]/ )
|
81
|
+
line << nextLine[1, nextLine.size]
|
82
|
+
elsif( nextLine =~ /^$/ )
|
83
|
+
else
|
84
|
+
@prev_line = nextLine
|
85
|
+
break
|
86
|
+
end
|
87
|
+
end
|
88
|
+
line
|
89
|
+
end
|
90
|
+
|
91
|
+
# Parse the calendar into an object representation
|
92
|
+
def parse
|
93
|
+
calendars = []
|
94
|
+
|
95
|
+
@@logger.debug "parsing..."
|
96
|
+
# Outer loop for Calendar objects
|
97
|
+
while (line = next_line)
|
98
|
+
fields = parse_line(line)
|
99
|
+
|
100
|
+
# Just iterate through until we find the beginning of a calendar object
|
101
|
+
if fields[:name] == "BEGIN" and fields[:value] == "VCALENDAR"
|
102
|
+
cal = parse_component
|
103
|
+
@@logger.debug "Added parsed calendar..."
|
104
|
+
calendars << cal
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
calendars
|
109
|
+
end
|
110
|
+
|
111
|
+
private
|
112
|
+
|
113
|
+
# Parse a single VCALENDAR object
|
114
|
+
# -- This should consist of the PRODID, VERSION, option METHOD & CALSCALE,
|
115
|
+
# and then one or more calendar components: VEVENT, VTODO, VJOURNAL,
|
116
|
+
# VFREEBUSY, VTIMEZONE
|
117
|
+
def parse_component(component = Calendar.new)
|
118
|
+
@@logger.debug "parsing new component..."
|
119
|
+
|
120
|
+
while (line = next_line)
|
121
|
+
fields = parse_line(line)
|
122
|
+
|
123
|
+
name = fields[:name].upcase
|
124
|
+
|
125
|
+
# Although properties are supposed to come before components, we should
|
126
|
+
# be able to handle them in any order...
|
127
|
+
if name == "END"
|
128
|
+
break
|
129
|
+
elsif name == "BEGIN" # New component
|
130
|
+
case(fields[:value])
|
131
|
+
when "VEVENT" # Event
|
132
|
+
component.add_component parse_component(Event.new)
|
133
|
+
when "VTODO" # Todo entry
|
134
|
+
component.add_component parse_component(Todo.new)
|
135
|
+
when "VALARM" # Alarm sub-component for event and todo
|
136
|
+
component.add_component parse_component(Alarm.new)
|
137
|
+
when "VJOURNAL" # Journal entry
|
138
|
+
component.add_component parse_component(Journal.new)
|
139
|
+
when "VFREEBUSY" # Free/Busy section
|
140
|
+
component.add_component parse_component(Freebusy.new)
|
141
|
+
when "VTIMEZONE" # Timezone specification
|
142
|
+
component.add_component parse_component(Timezone.new)
|
143
|
+
when "STANDARD" # Standard time sub-component for timezone
|
144
|
+
component.add_component parse_component(Standard.new)
|
145
|
+
when "DAYLIGHT" # Daylight time sub-component for timezone
|
146
|
+
component.add_component parse_component(Daylight.new)
|
147
|
+
else # Uknown component type, skip to matching end
|
148
|
+
until ((line = next_line) == "END:#{fields[:value]}"); end
|
149
|
+
next
|
150
|
+
end
|
151
|
+
else # If its not a component then it should be a property
|
152
|
+
params = fields[:params]
|
153
|
+
value = fields[:value]
|
154
|
+
|
155
|
+
# Lookup the property name to see if we have a string to
|
156
|
+
# object parser for this property type.
|
157
|
+
orig_value = value
|
158
|
+
if @parsers.has_key?(name)
|
159
|
+
value = @parsers[name].call(name, params, value)
|
160
|
+
end
|
161
|
+
|
162
|
+
name = name.downcase
|
163
|
+
|
164
|
+
# TODO: check to see if there are any more conflicts.
|
165
|
+
if name == 'class' or name == 'method'
|
166
|
+
name = "ip_" + name
|
167
|
+
end
|
168
|
+
|
169
|
+
# Replace dashes with underscores
|
170
|
+
name = name.gsub('-', '_')
|
171
|
+
|
172
|
+
if component.multi_property?(name)
|
173
|
+
adder = "add_" + name
|
174
|
+
if component.respond_to?(adder)
|
175
|
+
component.send(adder, value, params)
|
176
|
+
else
|
177
|
+
raise(UnknownPropertyMethod, "Unknown property type: #{adder}")
|
178
|
+
end
|
179
|
+
else
|
180
|
+
if component.respond_to?(name)
|
181
|
+
component.send(name, value, params)
|
182
|
+
else
|
183
|
+
raise(UnknownPropertyMethod, "Unknown property type: #{name}")
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
component
|
190
|
+
end
|
191
|
+
|
192
|
+
# 1*(ALPHA / DIGIT / "=")
|
193
|
+
NAME = '[-a-z0-9]+'
|
194
|
+
|
195
|
+
# <"> <Any character except CTLs, DQUOTE> <">
|
196
|
+
QSTR = '"[^"]*"'
|
197
|
+
|
198
|
+
# Contentline
|
199
|
+
LINE = "(#{NAME})(.*(?:#{QSTR})|(?:[^:]*))\:(.*)"
|
200
|
+
|
201
|
+
# *<Any character except CTLs, DQUOTE, ";", ":", ",">
|
202
|
+
PTEXT = '[^";:,]*'
|
203
|
+
|
204
|
+
# param-value = ptext / quoted-string
|
205
|
+
PVALUE = "#{QSTR}|#{PTEXT}"
|
206
|
+
|
207
|
+
# param = name "=" param-value *("," param-value)
|
208
|
+
PARAM = ";(#{NAME})(=?)((?:#{PVALUE})(?:,#{PVALUE})*)"
|
209
|
+
|
210
|
+
def parse_line(line)
|
211
|
+
unless line =~ %r{#{LINE}}i # Case insensitive match for a valid line
|
212
|
+
raise "Invalid line in calendar string!"
|
213
|
+
end
|
214
|
+
|
215
|
+
name = $1.upcase # The case insensitive part is upcased for easier comparison...
|
216
|
+
paramslist = $2
|
217
|
+
value = $3.gsub("\\;", ";").gsub("\\,", ",").gsub("\\n", "\n").gsub("\\\\", "\\")
|
218
|
+
|
219
|
+
# Parse the parameters
|
220
|
+
params = {}
|
221
|
+
if paramslist.size > 1
|
222
|
+
paramslist.scan( %r{#{PARAM}}i ) do
|
223
|
+
|
224
|
+
# parameter names are case-insensitive, and multi-valued
|
225
|
+
pname = $1
|
226
|
+
pvals = $3
|
227
|
+
|
228
|
+
# If there isn't an '=' sign then we need to do some custom
|
229
|
+
# business. Defaults to 'type'
|
230
|
+
if $2 == ""
|
231
|
+
pvals = $1
|
232
|
+
case $1
|
233
|
+
when /quoted-printable/i
|
234
|
+
pname = 'encoding'
|
235
|
+
|
236
|
+
when /base64/i
|
237
|
+
pname = 'encoding'
|
238
|
+
|
239
|
+
else
|
240
|
+
pname = 'type'
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
# Make entries into the params dictionary where the name
|
245
|
+
# is the key and the value is an array of values.
|
246
|
+
unless params.key? pname
|
247
|
+
params[pname] = []
|
248
|
+
end
|
249
|
+
|
250
|
+
# Save all the values into the array.
|
251
|
+
pvals.scan( %r{(#{PVALUE})} ) do
|
252
|
+
if $1.size > 0
|
253
|
+
params[pname] << $1
|
254
|
+
end
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
{:name => name, :value => value, :params => params}
|
260
|
+
end
|
261
|
+
|
262
|
+
## Following is a collection of parsing functions for various
|
263
|
+
## icalendar property value data types... First we setup
|
264
|
+
## a hash with property names pointing to methods...
|
265
|
+
def setup_parsers
|
266
|
+
@parsers = {}
|
267
|
+
|
268
|
+
# Integer properties
|
269
|
+
m = self.method(:parse_integer)
|
270
|
+
@parsers["PERCENT-COMPLETE"] = m
|
271
|
+
@parsers["PRIORITY"] = m
|
272
|
+
@parsers["REPEAT"] = m
|
273
|
+
@parsers["SEQUENCE"] = m
|
274
|
+
|
275
|
+
# Dates and Times
|
276
|
+
m = self.method(:parse_datetime)
|
277
|
+
@parsers["COMPLETED"] = m
|
278
|
+
@parsers["DTEND"] = m
|
279
|
+
@parsers["DUE"] = m
|
280
|
+
@parsers["DTSTART"] = m
|
281
|
+
@parsers["RECURRENCE-ID"] = m
|
282
|
+
@parsers["EXDATE"] = m
|
283
|
+
@parsers["RDATE"] = m
|
284
|
+
@parsers["CREATED"] = m
|
285
|
+
@parsers["DTSTAMP"] = m
|
286
|
+
@parsers["LAST-MODIFIED"] = m
|
287
|
+
|
288
|
+
# URI's
|
289
|
+
m = self.method(:parse_uri)
|
290
|
+
@parsers["TZURL"] = m
|
291
|
+
@parsers["ATTENDEE"] = m
|
292
|
+
@parsers["ORGANIZER"] = m
|
293
|
+
@parsers["URL"] = m
|
294
|
+
|
295
|
+
# This is a URI by default, and if its not a valid URI
|
296
|
+
# it will be returned as a string which works for binary data
|
297
|
+
# the other possible type.
|
298
|
+
@parsers["ATTACH"] = m
|
299
|
+
|
300
|
+
# GEO
|
301
|
+
m = self.method(:parse_geo)
|
302
|
+
@parsers["GEO"] = m
|
303
|
+
|
304
|
+
#RECUR
|
305
|
+
m = self.method(:parse_recur)
|
306
|
+
@parsers["RRULE"] = m
|
307
|
+
@parsers["EXRULE"] = m
|
308
|
+
|
309
|
+
end
|
310
|
+
|
311
|
+
# Booleans
|
312
|
+
# NOTE: It appears that although this is a valid data type
|
313
|
+
# there aren't any properties that use it... Maybe get
|
314
|
+
# rid of this in the future.
|
315
|
+
def parse_boolean(name, params, value)
|
316
|
+
if value.upcase == "FALSE"
|
317
|
+
false
|
318
|
+
else
|
319
|
+
true
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
# Dates, Date-Times & Times
|
324
|
+
# NOTE: invalid dates & times will be returned as strings...
|
325
|
+
def parse_datetime(name, params, value)
|
326
|
+
begin
|
327
|
+
if params["VALUE"] && params["VALUE"].first == "DATE"
|
328
|
+
result = Date.parse(value)
|
329
|
+
else
|
330
|
+
result = DateTime.parse(value)
|
331
|
+
if /Z$/ =~ value
|
332
|
+
timezone = "UTC"
|
333
|
+
else
|
334
|
+
timezone = params["TZID"].first if params["TZID"]
|
335
|
+
end
|
336
|
+
result.icalendar_tzid = timezone
|
337
|
+
end
|
338
|
+
result
|
339
|
+
rescue Exception
|
340
|
+
value
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
def parse_recur(name, params, value)
|
345
|
+
::Icalendar::RRule.new(name, params, value, self)
|
346
|
+
end
|
347
|
+
|
348
|
+
# Durations
|
349
|
+
# TODO: Need to figure out the best way to represent durations
|
350
|
+
# so just returning string for now.
|
351
|
+
def parse_duration(name, params, value)
|
352
|
+
value
|
353
|
+
end
|
354
|
+
|
355
|
+
# Floats
|
356
|
+
# NOTE: returns 0.0 if it can't parse the value
|
357
|
+
def parse_float(name, params, value)
|
358
|
+
value.to_f
|
359
|
+
end
|
360
|
+
|
361
|
+
# Integers
|
362
|
+
# NOTE: returns 0 if it can't parse the value
|
363
|
+
def parse_integer(name, params, value)
|
364
|
+
value.to_i
|
365
|
+
end
|
366
|
+
|
367
|
+
# Periods
|
368
|
+
# TODO: Got to figure out how to represent periods also...
|
369
|
+
def parse_period(name, params, value)
|
370
|
+
value
|
371
|
+
end
|
372
|
+
|
373
|
+
# Calendar Address's & URI's
|
374
|
+
# NOTE: invalid URI's will be returned as strings...
|
375
|
+
def parse_uri(name, params, value)
|
376
|
+
begin
|
377
|
+
URI.parse(value)
|
378
|
+
rescue Exception
|
379
|
+
value
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
# Geographical location (GEO)
|
384
|
+
# NOTE: returns an array with two floats (long & lat)
|
385
|
+
# if the parsing fails return the string
|
386
|
+
def parse_geo(name, params, value)
|
387
|
+
strloc = value.split(';')
|
388
|
+
if strloc.size != 2
|
389
|
+
return value
|
390
|
+
end
|
391
|
+
|
392
|
+
Geo.new(strloc[0].to_f, strloc[1].to_f)
|
393
|
+
end
|
394
|
+
|
395
|
+
end
|
396
|
+
end
|