icalendar 0.95 → 0.96

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