icalendar 0.95

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/COPYING +56 -0
  2. data/GPL +340 -0
  3. data/README +121 -0
  4. data/Rakefile +101 -0
  5. data/docs/api/classes/Array.html +146 -0
  6. data/docs/api/classes/Date.html +157 -0
  7. data/docs/api/classes/DateTime.html +178 -0
  8. data/docs/api/classes/Fixnum.html +146 -0
  9. data/docs/api/classes/Float.html +146 -0
  10. data/docs/api/classes/Icalendar/Alarm.html +184 -0
  11. data/docs/api/classes/Icalendar/Base.html +118 -0
  12. data/docs/api/classes/Icalendar/Calendar.html +411 -0
  13. data/docs/api/classes/Icalendar/Component.html +306 -0
  14. data/docs/api/classes/Icalendar/DateProp.html +187 -0
  15. data/docs/api/classes/Icalendar/DateProp/ClassMethods.html +195 -0
  16. data/docs/api/classes/Icalendar/Event.html +202 -0
  17. data/docs/api/classes/Icalendar/Freebusy.html +157 -0
  18. data/docs/api/classes/Icalendar/InvalidComponentClass.html +117 -0
  19. data/docs/api/classes/Icalendar/InvalidPropertyValue.html +117 -0
  20. data/docs/api/classes/Icalendar/Journal.html +190 -0
  21. data/docs/api/classes/Icalendar/Parameter.html +166 -0
  22. data/docs/api/classes/Icalendar/Parser.html +447 -0
  23. data/docs/api/classes/Icalendar/Timezone.html +197 -0
  24. data/docs/api/classes/Icalendar/Todo.html +199 -0
  25. data/docs/api/classes/String.html +160 -0
  26. data/docs/api/classes/Time.html +161 -0
  27. data/docs/api/created.rid +1 -0
  28. data/docs/api/files/COPYING.html +163 -0
  29. data/docs/api/files/GPL.html +531 -0
  30. data/docs/api/files/README.html +241 -0
  31. data/docs/api/files/lib/icalendar/base_rb.html +108 -0
  32. data/docs/api/files/lib/icalendar/calendar_rb.html +101 -0
  33. data/docs/api/files/lib/icalendar/component/alarm_rb.html +101 -0
  34. data/docs/api/files/lib/icalendar/component/event_rb.html +101 -0
  35. data/docs/api/files/lib/icalendar/component/freebusy_rb.html +101 -0
  36. data/docs/api/files/lib/icalendar/component/journal_rb.html +101 -0
  37. data/docs/api/files/lib/icalendar/component/timezone_rb.html +101 -0
  38. data/docs/api/files/lib/icalendar/component/todo_rb.html +101 -0
  39. data/docs/api/files/lib/icalendar/component_rb.html +101 -0
  40. data/docs/api/files/lib/icalendar/conversions_rb.html +108 -0
  41. data/docs/api/files/lib/icalendar/helpers_rb.html +101 -0
  42. data/docs/api/files/lib/icalendar/parameter_rb.html +101 -0
  43. data/docs/api/files/lib/icalendar/parser_rb.html +109 -0
  44. data/docs/api/files/lib/icalendar_rb.html +118 -0
  45. data/docs/api/fr_class_index.html +48 -0
  46. data/docs/api/fr_file_index.html +43 -0
  47. data/docs/api/fr_method_index.html +63 -0
  48. data/docs/api/index.html +24 -0
  49. data/docs/api/rdoc-style.css +208 -0
  50. data/docs/examples/create_cal.rb +40 -0
  51. data/docs/examples/parse_cal.rb +19 -0
  52. data/docs/examples/single_event.ics +18 -0
  53. data/docs/rfcs/rfc2425.pdf +0 -0
  54. data/docs/rfcs/rfc2426.pdf +0 -0
  55. data/docs/rfcs/rfc2445.pdf +0 -0
  56. data/docs/rfcs/rfc2446.pdf +0 -0
  57. data/docs/rfcs/rfc2447.pdf +0 -0
  58. data/docs/rfcs/rfc3283.txt +738 -0
  59. data/lib/icalendar.rb +27 -0
  60. data/lib/icalendar/#helpers.rb# +92 -0
  61. data/lib/icalendar/base.rb +31 -0
  62. data/lib/icalendar/calendar.rb +112 -0
  63. data/lib/icalendar/component.rb +253 -0
  64. data/lib/icalendar/component/alarm.rb +35 -0
  65. data/lib/icalendar/component/event.rb +68 -0
  66. data/lib/icalendar/component/freebusy.rb +35 -0
  67. data/lib/icalendar/component/journal.rb +61 -0
  68. data/lib/icalendar/component/timezone.rb +43 -0
  69. data/lib/icalendar/component/todo.rb +65 -0
  70. data/lib/icalendar/conversions.rb +115 -0
  71. data/lib/icalendar/helpers.rb +109 -0
  72. data/lib/icalendar/parameter.rb +33 -0
  73. data/lib/icalendar/parser.rb +395 -0
  74. data/test/calendar_test.rb +46 -0
  75. data/test/component/event_test.rb +47 -0
  76. data/test/component_test.rb +74 -0
  77. data/test/parser_test.rb +83 -0
  78. data/test/property_helpers.rb +35 -0
  79. data/test/simplecal.ics +119 -0
  80. data/test/single_event.ics +23 -0
  81. metadata +135 -0
@@ -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,395 @@
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
+
13
+ module Icalendar
14
+ class Parser < Icalendar::Base
15
+ # 1*(ALPHA / DIGIT / "=")
16
+ NAME = '[-a-z0-9]+'
17
+
18
+ # <"> <Any character except CTLs, DQUOTE> <">
19
+ QSTR = '"[^"]*"'
20
+
21
+ # *<Any character except CTLs, DQUOTE, ";", ":", ",">
22
+ PTEXT = '[^";:,]*'
23
+
24
+ # param-value = ptext / quoted-string
25
+ PVALUE = "#{PTEXT}|#{QSTR}"
26
+
27
+ # Contentline
28
+ LINE = "(#{NAME})([^:]*)\:(.*)"
29
+
30
+ # param = name "=" param-value *("," param-value)
31
+ PARAM = ";(#{NAME})(=?)((?:#{PVALUE})(?:,#{PVALUE})*)"
32
+
33
+ # date = date-fullyear ["-"] date-month ["-"] date-mday
34
+ # date-fullyear = 4 DIGIT
35
+ # date-month = 2 DIGIT
36
+ # date-mday = 2 DIGIT
37
+ DATE = '(\d\d\d\d)-?(\d\d)-?(\d\d)'
38
+
39
+ # time = time-hour [":"] time-minute [":"] time-second [time-secfrac] [time-zone]
40
+ # time-hour = 2 DIGIT
41
+ # time-minute = 2 DIGIT
42
+ # time-second = 2 DIGIT
43
+ # time-secfrac = "," 1*DIGIT
44
+ # time-zone = "Z" / time-numzone
45
+ # time-numzome = sign time-hour [":"] time-minute
46
+ TIME = '(\d\d):?(\d\d):?(\d\d)(\.\d+)?(Z|[-+]\d\d:?\d\d)?'
47
+
48
+ def initialize(src)
49
+ @@logger.debug("New Calendar Parser")
50
+
51
+ # Setup the parser method hash table
52
+ setup_parsers()
53
+
54
+ # Define the next line method different depending on whether
55
+ # this is a string or an IO object so we can be efficient about
56
+ # parsing large files...
57
+
58
+ # Just do the unfolding work in one shot if its a whole string
59
+ if src.respond_to?(:split)
60
+ unfolded = []
61
+
62
+ # Split into an array of lines, then unfold those into a new array
63
+ src.split(/\r?\n/).each do |line|
64
+
65
+ # If it's a continuation line, add it to the last.
66
+ # If it's an empty line, drop it from the input.
67
+ if( line =~ /^[ \t]/ )
68
+ unfolded << unfolded.pop + line[1, line.size-1]
69
+ elsif( line =~ /^$/ )
70
+ else
71
+ unfolded << line
72
+ end
73
+ end
74
+
75
+ @lines = unfolded
76
+ @index = 0
77
+
78
+ # Now that we are unfolded we can just iterate through the array.
79
+ # Dynamically define next line for a string.
80
+ def next_line
81
+ if @index == @lines.size
82
+ return nil
83
+ else
84
+ line = @lines[@index]
85
+ @index += 1
86
+ return line
87
+ end
88
+ end
89
+
90
+ # If its a file we need to read and unfold on the go to save from reading
91
+ # large amounts of data into memory.
92
+ elsif src.respond_to?(:gets)
93
+ @file = src
94
+ @prev_line = src.gets
95
+ if !@prev_line.nil?
96
+ @prev_line.chomp!
97
+ end
98
+
99
+ # Dynamically define next line for an IO object
100
+ def next_line
101
+ line = @prev_line
102
+
103
+ if line.nil?
104
+ return nil
105
+ end
106
+
107
+ # Loop through until we get to a non-continuation line...
108
+ loop do
109
+ nextLine = @file.gets
110
+ if !nextLine.nil?
111
+ nextLine.chomp!
112
+ end
113
+
114
+ # If it's a continuation line, add it to the last.
115
+ # If it's an empty line, drop it from the input.
116
+ if( nextLine =~ /^[ \t]/ )
117
+ line << nextLine[1, nextLine.size]
118
+ elsif( nextLine =~ /^$/ )
119
+ else
120
+ @prev_line = nextLine
121
+ break
122
+ end
123
+ end
124
+ line
125
+ end
126
+ else
127
+ raise ArgumentError, "CalendarParser.new cannot be called with a #{src.class} type!"
128
+ end
129
+ end
130
+
131
+ # Parse the calendar into an object representation
132
+ def parse
133
+ calendars = []
134
+
135
+ # Outer loop for Calendar objects
136
+ while (line = next_line)
137
+ fields = parse_line(line)
138
+
139
+ # Just iterate through until we find the beginning of a calendar object
140
+ if fields[:name] == "BEGIN" and fields[:value] == "VCALENDAR"
141
+ cal = parse_component
142
+ calendars << cal
143
+ end
144
+ end
145
+
146
+ calendars
147
+ end
148
+
149
+ private
150
+
151
+ # Parse a single VCALENDAR object
152
+ # -- This should consist of the PRODID, VERSION, option METHOD & CALSCALE,
153
+ # and then one or more calendar components: VEVENT, VTODO, VJOURNAL,
154
+ # VFREEBUSY, VTIMEZONE
155
+ def parse_component(component = Calendar.new)
156
+ while (line = next_line)
157
+ fields = parse_line(line)
158
+
159
+ name = fields[:name]
160
+
161
+ # Although properties are supposed to come before components, we should
162
+ # be able to handle them in any order...
163
+ if name == "END"
164
+ break
165
+ elsif name == "BEGIN" # New component
166
+ case(fields[:value])
167
+ when "VEVENT"
168
+ component.events << parse_component(Event.new)
169
+ when "VTODO"
170
+ component.todos << parse_component(Todo.new)
171
+ when "VJOURNAL"
172
+ component.journals << parse_component(Journal.new)
173
+ when "VFREEBUSY"
174
+ component.freebusys << parse_component(Freebusy.new)
175
+ when "VTIMEZONE"
176
+ component.timezones << parse_component(Timezone.new)
177
+ when "VALARM"
178
+ component.alarms << parse_component(Alarm.new)
179
+ end
180
+ else # If its not a component then it should be a property
181
+
182
+ # Just set the properties so that the parser can still
183
+ # parse invalid files...
184
+ @@logger.debug("Setting #{name} => #{fields[:value]}")
185
+
186
+ # Lookup the property name to see if we have a string to
187
+ # object parser for this property type.
188
+ if @parsers.has_key?(name.upcase)
189
+ val = @parsers[name.upcase].call(name, fields[:params], fields[:value])
190
+ else
191
+ val = fields[:value]
192
+ end
193
+
194
+ if component.multi_property?(name.upcase) && component.properties
195
+ val = [val]
196
+
197
+ if fields[:params].empty?
198
+ params = [nil]
199
+ else
200
+ params = fields[:params]
201
+ end
202
+
203
+ if component.properties.has_key?(name)
204
+ component.properties[name] += val
205
+ component.property_params[name] += params
206
+ else
207
+ component.properties[name] = val
208
+ component.property_params[name] = params
209
+ end
210
+
211
+ else
212
+ component.properties[name] = val
213
+
214
+ unless fields[:params].empty?
215
+ component.property_params[name] = fields[:params]
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ component
222
+
223
+ end
224
+
225
+ def parse_line(line)
226
+ unless line =~ %r{#{LINE}}i # Case insensitive match for a valid line
227
+ raise "Invalid line in calendar string!"
228
+ end
229
+
230
+ name = $1.upcase # The case insensitive part is upcased for easier comparison...
231
+ paramslist = $2
232
+ value = $3
233
+
234
+ params = {}
235
+
236
+ # Collect the params, if any.
237
+ if paramslist.size > 1
238
+
239
+ # v3.0 and v2.1 params
240
+ paramslist.scan( %r{#{PARAM}}i ) do
241
+
242
+ # param names are case-insensitive, and multi-valued
243
+ pname = $1
244
+ pvals = $3
245
+
246
+ # v2.1 pvals have no '=' sign, figure out what kind of param it
247
+ # is (either its a known encoding, or we treat it as a 'type'
248
+ # param).
249
+ if $2 == ""
250
+ pvals = $1
251
+ case $1
252
+ when /quoted-printable/i
253
+ pname = 'encoding'
254
+
255
+ when /base64/i
256
+ pname = 'encoding'
257
+
258
+ else
259
+ pname = 'type'
260
+ end
261
+ end
262
+
263
+ unless params.key? pname
264
+ params[pname] = []
265
+ end
266
+ pvals.scan( %r{(#{PVALUE})} ) do
267
+ if $1.size > 0
268
+ params[pname] << $1
269
+ end
270
+ end
271
+ end
272
+ end
273
+
274
+ {:name => name, :params => params, :value => value}
275
+ end
276
+
277
+ ## Following is a collection of parsing functions for various
278
+ ## icalendar property value data types... First we setup
279
+ ## a hash with property names pointing to methods...
280
+ def setup_parsers
281
+ @parsers = {}
282
+
283
+ # Integer properties
284
+ m = self.method(:parse_integer)
285
+ @parsers["PERCENT-COMPLETE"] = m
286
+ @parsers["PRIORITY"] = m
287
+ @parsers["REPEAT"] = m
288
+ @parsers["SEQUENCE"] = m
289
+
290
+ # Dates and Times
291
+ m = self.method(:parse_datetime)
292
+ @parsers["COMPLETED"] = m
293
+ @parsers["DTEND"] = m
294
+ @parsers["DUE"] = m
295
+ @parsers["DTSTART"] = m
296
+ @parsers["RECURRENCE-ID"] = m
297
+ @parsers["EXDATE"] = m
298
+ @parsers["RDATE"] = m
299
+ @parsers["CREATED"] = m
300
+ @parsers["DTSTAMP"] = m
301
+ @parsers["LAST-MODIFIED"] = m
302
+
303
+ # URI's
304
+ m = self.method(:parse_uri)
305
+ @parsers["TZURL"] = m
306
+ @parsers["ATTENDEE"] = m
307
+ @parsers["ORGANIZER"] = m
308
+ @parsers["URL"] = m
309
+
310
+ # This is a URI by default, and if its not a valid URI
311
+ # it will be returned as a string which works for binary data
312
+ # the other possible type.
313
+ @parsers["ATTACH"] = m
314
+
315
+ # GEO
316
+ m = self.method(:parse_geo)
317
+ @parsers["GEO"] = m
318
+
319
+ end
320
+
321
+ # Booleans
322
+ # NOTE: It appears that although this is a valid data type
323
+ # there aren't any properties that use it... Maybe get
324
+ # rid of this in the future.
325
+ def parse_boolean(name, params, value)
326
+ if value.upcase == "FALSE"
327
+ false
328
+ else
329
+ true
330
+ end
331
+ end
332
+
333
+ # Dates, Date-Times & Times
334
+ # NOTE: invalid dates & times will be returned as strings...
335
+ def parse_datetime(name, params, value)
336
+ begin
337
+ DateTime.parse(value)
338
+ rescue Exception
339
+ value
340
+ end
341
+
342
+ end
343
+
344
+ # Durations
345
+ # TODO: Need to figure out the best way to represent durations
346
+ # so just returning string for now.
347
+ def parse_duration(name, params, value)
348
+ value
349
+ end
350
+
351
+ # Floats
352
+ # NOTE: returns 0.0 if it can't parse the value
353
+ def parse_float(name, params, value)
354
+ value.to_f
355
+ end
356
+
357
+ # Integers
358
+ # NOTE: returns 0 if it can't parse the value
359
+ def parse_integer(name, params, value)
360
+ value.to_i
361
+ end
362
+
363
+ # Periods
364
+ # TODO: Got to figure out how to represent periods also...
365
+ def parse_period(name, params, value)
366
+ value
367
+ end
368
+
369
+ # Calendar Address's & URI's
370
+ # NOTE: invalid URI's will be returned as strings...
371
+ def parse_uri(name, params, value)
372
+ begin
373
+ URI.parse(value)
374
+ rescue Exception
375
+ value
376
+ end
377
+ end
378
+
379
+ # Geographical location (GEO)
380
+ # NOTE: returns an array with two floats (long & lat)
381
+ # if the parsing fails return the string
382
+ def parse_geo(name, params, value)
383
+ strloc = value.split(';')
384
+ if strloc.size != 2
385
+ return value
386
+ end
387
+
388
+ val = []
389
+ val[0] = strloc[0].to_f
390
+ val[1] = strloc[1].to_f
391
+ val
392
+ end
393
+
394
+ end
395
+ end
@@ -0,0 +1,46 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib')
2
+
3
+ require 'test/unit'
4
+ require 'icalendar'
5
+
6
+ require 'date'
7
+
8
+ class TestCalendar < Test::Unit::TestCase
9
+ # Generate a calendar using the raw api, and then spit it out
10
+ # as a string. Parse the string and make sure everything matches up.
11
+ def test_raw_generation
12
+ # Create a fresh calendar
13
+ cal = Icalendar::Calendar.new
14
+
15
+ cal.calscale = "GREGORIAN"
16
+ cal.version = "3.2"
17
+ cal.prodid = "test-prodid"
18
+
19
+ # Now generate the string and then parse it so we can verify
20
+ # that everything was set, generated and parsed correctly.
21
+ calString = cal.to_ical
22
+
23
+ cals = Icalendar::Parser.new(calString).parse
24
+
25
+ cal2 = cals.first
26
+ assert_equal("GREGORIAN", cal2.calscale)
27
+ assert_equal("3.2", cal2.version)
28
+ assert_equal("test-prodid", cal2.prodid)
29
+ end
30
+
31
+ def test_block_creation
32
+ cal = Icalendar::Calendar.new
33
+ cal.event do
34
+ dtend "19970903T190000Z"
35
+ summary "This is my summary"
36
+ end
37
+
38
+ event = cal.event
39
+ event.dtend = "19970903T190000Z"
40
+ event.summary = "This is my summary"
41
+
42
+ ev = cal.events.each do |ev|
43
+ assert_equal("19970903T190000Z", ev.dtend)
44
+ end
45
+ end
46
+ end