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.
Files changed (44) hide show
  1. data/COPYING +56 -0
  2. data/GPL +340 -0
  3. data/README +266 -0
  4. data/Rakefile +110 -0
  5. data/docs/rfcs/itip_notes.txt +69 -0
  6. data/docs/rfcs/rfc2425.pdf +0 -0
  7. data/docs/rfcs/rfc2426.pdf +0 -0
  8. data/docs/rfcs/rfc2445.pdf +0 -0
  9. data/docs/rfcs/rfc2446.pdf +0 -0
  10. data/docs/rfcs/rfc2447.pdf +0 -0
  11. data/docs/rfcs/rfc3283.txt +738 -0
  12. data/examples/create_cal.rb +45 -0
  13. data/examples/parse_cal.rb +20 -0
  14. data/examples/single_event.ics +18 -0
  15. data/lib/hash_attrs.rb +34 -0
  16. data/lib/icalendar.rb +39 -0
  17. data/lib/icalendar/base.rb +43 -0
  18. data/lib/icalendar/calendar.rb +113 -0
  19. data/lib/icalendar/component.rb +442 -0
  20. data/lib/icalendar/component/alarm.rb +44 -0
  21. data/lib/icalendar/component/event.rb +129 -0
  22. data/lib/icalendar/component/freebusy.rb +38 -0
  23. data/lib/icalendar/component/journal.rb +61 -0
  24. data/lib/icalendar/component/timezone.rb +105 -0
  25. data/lib/icalendar/component/todo.rb +64 -0
  26. data/lib/icalendar/conversions.rb +150 -0
  27. data/lib/icalendar/helpers.rb +109 -0
  28. data/lib/icalendar/parameter.rb +33 -0
  29. data/lib/icalendar/parser.rb +396 -0
  30. data/lib/meta.rb +32 -0
  31. data/test/calendar_test.rb +71 -0
  32. data/test/component/event_test.rb +256 -0
  33. data/test/component/todo_test.rb +13 -0
  34. data/test/component_test.rb +76 -0
  35. data/test/conversions_test.rb +97 -0
  36. data/test/fixtures/folding.ics +23 -0
  37. data/test/fixtures/life.ics +46 -0
  38. data/test/fixtures/simplecal.ics +119 -0
  39. data/test/fixtures/single_event.ics +23 -0
  40. data/test/interactive.rb +17 -0
  41. data/test/parameter_test.rb +29 -0
  42. data/test/parser_test.rb +84 -0
  43. data/test/read_write.rb +23 -0
  44. 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