paulsm-icalendar 1.1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/COPYING +56 -0
  2. data/GPL +340 -0
  3. data/README +266 -0
  4. data/Rakefile +109 -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/icalendar/rrule.rb +126 -0
  31. data/lib/icalendar/tzinfo.rb +121 -0
  32. data/lib/meta.rb +32 -0
  33. data/test/calendar_test.rb +71 -0
  34. data/test/component/event_test.rb +256 -0
  35. data/test/component/timezone_test.rb +67 -0
  36. data/test/component/todo_test.rb +13 -0
  37. data/test/component_test.rb +76 -0
  38. data/test/conversions_test.rb +97 -0
  39. data/test/coverage/STUB +0 -0
  40. data/test/fixtures/folding.ics +23 -0
  41. data/test/fixtures/life.ics +46 -0
  42. data/test/fixtures/simplecal.ics +119 -0
  43. data/test/fixtures/single_event.ics +23 -0
  44. data/test/interactive.rb +17 -0
  45. data/test/parameter_test.rb +29 -0
  46. data/test/parser_test.rb +84 -0
  47. data/test/read_write.rb +23 -0
  48. metadata +108 -0
@@ -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