icalendar 0.95

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 (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