icalendar 1.5.4 → 2.0.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -1
  3. data/.rspec +2 -0
  4. data/.travis.yml +1 -2
  5. data/History.txt +2 -7
  6. data/README.md +82 -107
  7. data/Rakefile +6 -7
  8. data/icalendar.gemspec +10 -9
  9. data/lib/icalendar.rb +17 -33
  10. data/lib/icalendar/alarm.rb +35 -0
  11. data/lib/icalendar/calendar.rb +17 -100
  12. data/lib/icalendar/component.rb +41 -403
  13. data/lib/icalendar/event.rb +51 -0
  14. data/lib/icalendar/freebusy.rb +27 -0
  15. data/lib/icalendar/has_components.rb +83 -0
  16. data/lib/icalendar/has_properties.rb +156 -0
  17. data/lib/icalendar/journal.rb +39 -0
  18. data/lib/icalendar/parser.rb +75 -403
  19. data/lib/icalendar/timezone.rb +53 -0
  20. data/lib/icalendar/todo.rb +52 -0
  21. data/lib/icalendar/tzinfo.rb +30 -30
  22. data/lib/icalendar/value.rb +80 -0
  23. data/lib/icalendar/values/array.rb +43 -0
  24. data/lib/icalendar/values/binary.rb +31 -0
  25. data/lib/icalendar/values/boolean.rb +17 -0
  26. data/lib/icalendar/values/cal_address.rb +8 -0
  27. data/lib/icalendar/values/date.rb +26 -0
  28. data/lib/icalendar/values/date_time.rb +34 -0
  29. data/lib/icalendar/values/duration.rb +48 -0
  30. data/lib/icalendar/values/float.rb +17 -0
  31. data/lib/icalendar/values/integer.rb +17 -0
  32. data/lib/icalendar/values/period.rb +46 -0
  33. data/lib/icalendar/values/recur.rb +63 -0
  34. data/lib/icalendar/values/text.rb +26 -0
  35. data/lib/icalendar/values/time.rb +34 -0
  36. data/lib/icalendar/values/time_with_zone.rb +31 -0
  37. data/lib/icalendar/values/uri.rb +19 -0
  38. data/lib/icalendar/values/utc_offset.rb +39 -0
  39. data/lib/icalendar/version.rb +5 -0
  40. data/spec/alarm_spec.rb +108 -0
  41. data/spec/calendar_spec.rb +167 -0
  42. data/spec/event_spec.rb +108 -0
  43. data/{test/fixtures/folding.ics → spec/fixtures/nondefault_values.ics} +2 -2
  44. data/{test → spec}/fixtures/single_event.ics +11 -14
  45. data/spec/fixtures/timezone.ics +35 -0
  46. data/spec/freebusy_spec.rb +7 -0
  47. data/spec/journal_spec.rb +7 -0
  48. data/spec/parser_spec.rb +26 -0
  49. data/spec/roundtrip_spec.rb +40 -0
  50. data/spec/spec_helper.rb +25 -0
  51. data/spec/timezone_spec.rb +31 -0
  52. data/spec/todo_spec.rb +24 -0
  53. data/spec/tzinfo_spec.rb +85 -0
  54. data/spec/values/date_time_spec.rb +80 -0
  55. data/spec/values/duration_spec.rb +67 -0
  56. data/spec/values/period_spec.rb +47 -0
  57. data/spec/values/recur_spec.rb +47 -0
  58. data/spec/values/text_spec.rb +72 -0
  59. data/spec/values/utc_offset_spec.rb +41 -0
  60. metadata +129 -88
  61. data/GPL +0 -340
  62. data/examples/create_cal.rb +0 -45
  63. data/examples/parse_cal.rb +0 -20
  64. data/examples/single_event.ics +0 -18
  65. data/lib/hash_attrs.rb +0 -34
  66. data/lib/icalendar/base.rb +0 -47
  67. data/lib/icalendar/component/alarm.rb +0 -47
  68. data/lib/icalendar/component/event.rb +0 -131
  69. data/lib/icalendar/component/freebusy.rb +0 -38
  70. data/lib/icalendar/component/journal.rb +0 -60
  71. data/lib/icalendar/component/timezone.rb +0 -91
  72. data/lib/icalendar/component/todo.rb +0 -64
  73. data/lib/icalendar/conversions.rb +0 -107
  74. data/lib/icalendar/helpers.rb +0 -109
  75. data/lib/icalendar/parameter.rb +0 -33
  76. data/lib/icalendar/rrule.rb +0 -133
  77. data/lib/meta.rb +0 -32
  78. data/script/console +0 -10
  79. data/script/recur1.ics +0 -38
  80. data/script/tryit.rb +0 -13
  81. data/test/component/test_event.rb +0 -253
  82. data/test/component/test_timezone.rb +0 -74
  83. data/test/component/test_todo.rb +0 -31
  84. data/test/fixtures/life.ics +0 -46
  85. data/test/fixtures/nonstandard.ics +0 -25
  86. data/test/fixtures/simplecal.ics +0 -119
  87. data/test/interactive.rb +0 -17
  88. data/test/read_write.rb +0 -23
  89. data/test/test_calendar.rb +0 -167
  90. data/test/test_component.rb +0 -102
  91. data/test/test_conversions.rb +0 -104
  92. data/test/test_helper.rb +0 -7
  93. data/test/test_parameter.rb +0 -91
  94. data/test/test_parser.rb +0 -100
  95. data/test/test_tzinfo.rb +0 -83
  96. data/website/index.html +0 -70
  97. data/website/index.txt +0 -38
  98. data/website/javascripts/rounded_corners_lite.inc.js +0 -285
  99. data/website/stylesheets/screen.css +0 -159
  100. data/website/template.html.erb +0 -50
@@ -0,0 +1,51 @@
1
+ module Icalendar
2
+
3
+ class Event < Component
4
+ required_property :dtstamp, Icalendar::Values::DateTime
5
+ required_property :uid
6
+ # dtstart only required if calendar's method is nil
7
+ required_property :dtstart, Icalendar::Values::DateTime,
8
+ ->(event, dtstart) { !dtstart.nil? || !(event.parent.nil? || event.parent.ip_method.nil?) }
9
+
10
+ optional_single_property :dtend, Icalendar::Values::DateTime
11
+ optional_single_property :duration, Icalendar::Values::Duration
12
+ mutually_exclusive_properties :dtend, :duration
13
+
14
+ optional_single_property :ip_class
15
+ optional_single_property :created, Icalendar::Values::DateTime
16
+ optional_single_property :description
17
+ optional_single_property :geo, Icalendar::Values::Float
18
+ optional_single_property :last_modified, Icalendar::Values::DateTime
19
+ optional_single_property :location
20
+ optional_single_property :organizer, Icalendar::Values::CalAddress
21
+ optional_single_property :priority, Icalendar::Values::Integer
22
+ optional_single_property :sequence, Icalendar::Values::Integer
23
+ optional_single_property :status
24
+ optional_single_property :summary
25
+ optional_single_property :transp
26
+ optional_single_property :url, Icalendar::Values::Uri
27
+ optional_single_property :recurrence_id, Icalendar::Values::DateTime
28
+
29
+ optional_property :rrule, Icalendar::Values::Recur, true
30
+ optional_property :attach, Icalendar::Values::Uri
31
+ optional_property :attendee, Icalendar::Values::CalAddress
32
+ optional_property :categories
33
+ optional_property :comment
34
+ optional_property :contact
35
+ optional_property :exdate, Icalendar::Values::DateTime
36
+ optional_property :request_status
37
+ optional_property :related_to
38
+ optional_property :resources
39
+ optional_property :rdate, Icalendar::Values::DateTime
40
+
41
+ component :alarm, false
42
+
43
+ def initialize
44
+ super 'event'
45
+ self.dtstamp = Icalendar::Values::DateTime.new Time.now.utc, 'tzid' => 'UTC'
46
+ self.uid = new_uid
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,27 @@
1
+ module Icalendar
2
+
3
+ class Freebusy < Component
4
+
5
+ required_property :dtstamp, Icalendar::Values::DateTime
6
+ required_property :uid
7
+
8
+ optional_single_property :contact
9
+ optional_single_property :dtstart, Icalendar::Values::DateTime
10
+ optional_single_property :dtend, Icalendar::Values::DateTime
11
+ optional_single_property :organizer, Icalendar::Values::CalAddress
12
+ optional_single_property :url, Icalendar::Values::Uri
13
+
14
+ optional_property :attendee, Icalendar::Values::CalAddress
15
+ optional_property :comment
16
+ optional_property :freebusy, Icalendar::Values::Period
17
+ optional_property :request_status
18
+
19
+ def initialize
20
+ super 'freebusy'
21
+ self.dtstamp = Icalendar::Values::DateTime.new Time.now.utc, 'tzid' => 'UTC'
22
+ self.uid = new_uid
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,83 @@
1
+ module Icalendar
2
+
3
+ module HasComponents
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.class_eval do
8
+ attr_reader :custom_components
9
+ end
10
+ end
11
+
12
+ def initialize(*args)
13
+ @custom_components = Hash.new { |h, k| h[k] = [] }
14
+ super
15
+ end
16
+
17
+ def add_component(c)
18
+ c.parent = self
19
+ yield c if block_given?
20
+ send("#{c.name.downcase}s") << c
21
+ c
22
+ end
23
+
24
+ def method_missing(method, *args, &block)
25
+ method_name = method.to_s
26
+ if method_name =~ /^add_(x_\w+)$/
27
+ component_name = $1
28
+ custom = args.first || Component.new(component_name, component_name.upcase)
29
+ custom_components[component_name] << custom
30
+ yield custom if block_given?
31
+ custom
32
+ else
33
+ super
34
+ end
35
+ end
36
+
37
+ def respond_to_missing?(method_name, include_private = false)
38
+ method_name.to_s.start_with?('add_x_') || super
39
+ end
40
+
41
+ module ClassMethods
42
+ def components
43
+ @components ||= []
44
+ end
45
+
46
+ def component(singular_name, find_by = :uid, klass = nil)
47
+ components = "#{singular_name}s"
48
+ self.components << components
49
+ component_var = "@#{components}"
50
+
51
+ define_method components do
52
+ if instance_variable_defined? component_var
53
+ instance_variable_get component_var
54
+ else
55
+ instance_variable_set component_var, []
56
+ end
57
+ end
58
+
59
+ define_method singular_name do |c = nil, &block|
60
+ if c.nil?
61
+ c = begin
62
+ klass ||= Icalendar.const_get singular_name.capitalize
63
+ klass.new
64
+ rescue NameError => ne
65
+ puts "WARN: #{ne.message}"
66
+ Component.new singular_name
67
+ end
68
+ end
69
+ add_component c, &block
70
+ end
71
+
72
+ define_method "find_#{singular_name}" do |id|
73
+ send(components).find { |c| c.send(find_by) == id }
74
+ end if find_by
75
+
76
+ define_method "add_#{singular_name}" do |c|
77
+ send singular_name, c
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ end
@@ -0,0 +1,156 @@
1
+ module Icalendar
2
+
3
+ module HasProperties
4
+
5
+ def self.included(base)
6
+ base.extend ClassMethods
7
+ base.class_eval do
8
+ attr_reader :custom_properties
9
+ end
10
+ end
11
+
12
+ def initialize(*args)
13
+ @custom_properties = Hash.new { |h, k| h[k] = [] }
14
+ super
15
+ end
16
+
17
+ def valid?(strict = false)
18
+ self.class.required_properties.each_pair do |prop, validator|
19
+ validator.call(self, send(prop)) or return false
20
+ end
21
+ self.class.mutex_properties.each do |mutexprops|
22
+ mutexprops.map { |p| send p }.compact.size > 1 and return false
23
+ end
24
+ if strict
25
+ self.class.suggested_single_properties.each do |single_prop|
26
+ send(single_prop).size > 1 and return false
27
+ end
28
+ end
29
+ true
30
+ end
31
+
32
+ def method_missing(method, *args, &block)
33
+ method_name = method.to_s
34
+ if method_name.start_with? 'x_'
35
+ if method_name.end_with? '='
36
+ if args.first.is_a? Icalendar::Value
37
+ custom_properties[method_name.chomp('=')] << args.first
38
+ else
39
+ custom_properties[method_name.chomp('=')] << Icalendar::Values::Text.new(args.first)
40
+ end
41
+ else
42
+ custom_properties[method_name]
43
+ end
44
+ else
45
+ super
46
+ end
47
+ end
48
+
49
+ def respond_to_missing?(method, include_private = false)
50
+ method.to_s.start_with?('x_') || super
51
+ end
52
+
53
+ module ClassMethods
54
+ def properties
55
+ single_properties + multiple_properties
56
+ end
57
+
58
+ def single_properties
59
+ @single_properties ||= []
60
+ end
61
+
62
+ def multiple_properties
63
+ @multiple_properties ||= []
64
+ end
65
+
66
+ def required_properties
67
+ @required_properties ||= {}
68
+ end
69
+
70
+ def suggested_single_properties
71
+ @suggested_single_properties ||= []
72
+ end
73
+
74
+ def mutex_properties
75
+ @mutex_properties ||= []
76
+ end
77
+
78
+ def default_property_types
79
+ @default_property_types ||= Hash.new { |h,k| Icalendar::Values::Text }
80
+ end
81
+
82
+ def required_property(prop, klass = Icalendar::Values::Text, validator = nil)
83
+ validator ||= ->(component, value) { !value.nil? }
84
+ self.required_properties[prop] = validator
85
+ single_property prop, klass
86
+ end
87
+
88
+ def required_multi_property(prop, klass = Icalendar::Values::Text, validator = nil)
89
+ validator ||= ->(component, value) { !value.compact.empty? }
90
+ self.required_properties[prop] = validator
91
+ multi_property prop, klass
92
+ end
93
+
94
+ def optional_single_property(prop, klass = Icalendar::Values::Text)
95
+ single_property prop, klass
96
+ end
97
+
98
+ def mutually_exclusive_properties(*properties)
99
+ self.mutex_properties << properties
100
+ end
101
+
102
+ def optional_property(prop, klass = Icalendar::Values::Text, suggested_single = false)
103
+ self.suggested_single_properties << prop if suggested_single
104
+ multi_property prop, klass
105
+ end
106
+
107
+ def single_property(prop, klass)
108
+ self.single_properties << prop.to_s
109
+ self.default_property_types[prop.to_s] = klass
110
+
111
+ define_method prop do
112
+ instance_variable_get "@#{prop}"
113
+ end
114
+
115
+ define_method "#{prop}=" do |value|
116
+ instance_variable_set "@#{prop}", map_property_value(value, klass, false)
117
+ end
118
+ end
119
+
120
+ def multi_property(prop, klass)
121
+ self.multiple_properties << prop.to_s
122
+ self.default_property_types[prop.to_s] = klass
123
+ property_var = "@#{prop}"
124
+
125
+ define_method "#{prop}=" do |value|
126
+ instance_variable_set property_var, [map_property_value(value, klass, true)].compact
127
+ end
128
+
129
+ define_method prop do
130
+ if instance_variable_defined? property_var
131
+ instance_variable_get property_var
132
+ else
133
+ send "#{prop}=", nil
134
+ end
135
+ end
136
+
137
+ define_method "append_#{prop}" do |value|
138
+ send(prop) << map_property_value(value, klass, true)
139
+ end
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def map_property_value(value, klass, multi_valued)
146
+ if value.nil? || value.is_a?(Icalendar::Value)
147
+ value
148
+ elsif value.is_a? ::Array
149
+ Icalendar::Values::Array.new value, klass, {}, {delimiter: (multi_valued ? ',' : ';')}
150
+ else
151
+ klass.new value
152
+ end
153
+ end
154
+
155
+ end
156
+ end
@@ -0,0 +1,39 @@
1
+ module Icalendar
2
+
3
+ class Journal < Component
4
+
5
+ required_property :dtstamp, Icalendar::Values::DateTime
6
+ required_property :uid
7
+
8
+ optional_single_property :ip_class
9
+ optional_single_property :created, Icalendar::Values::DateTime
10
+ optional_single_property :dtstart, Icalendar::Values::DateTime
11
+ optional_single_property :last_modified, Icalendar::Values::DateTime
12
+ optional_single_property :organizer, Icalendar::Values::CalAddress
13
+ optional_single_property :recurrence_id, Icalendar::Values::DateTime
14
+ optional_single_property :sequence, Icalendar::Values::Integer
15
+ optional_single_property :status
16
+ optional_single_property :summary
17
+ optional_single_property :url, Icalendar::Values::Uri
18
+
19
+ optional_property :rrule, Icalendar::Values::Recur, true
20
+ optional_property :attach, Icalendar::Values::Uri
21
+ optional_property :attendee, Icalendar::Values::CalAddress
22
+ optional_property :categories
23
+ optional_property :comment
24
+ optional_property :contact
25
+ optional_property :description
26
+ optional_property :exdate, Icalendar::Values::DateTime
27
+ optional_property :request_status
28
+ optional_property :related_to
29
+ optional_property :rdate, Icalendar::Values::DateTime
30
+
31
+ def initialize
32
+ super 'journal'
33
+ self.dtstamp = Icalendar::Values::DateTime.new Time.now.utc, 'tzid' => 'UTC'
34
+ self.uid = new_uid
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -1,441 +1,113 @@
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
1
  module Icalendar
15
2
 
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
- # Defines if this is a strict parser.
43
- attr_accessor :strict
44
-
45
- def initialize(src, strict = true)
46
- # Setup the parser method hash table
47
- setup_parsers
48
-
49
- # The default behavior is to raise an error when the parser
50
- # finds an unknown property. Set this to false to discard
51
- # unknown properties instead of raising an error.
52
- @strict = strict
53
-
54
- if src.respond_to?(:gets)
55
- @file = src
56
- elsif (not src.nil?) and src.respond_to?(:to_s)
57
- @file = StringIO.new(src.to_s, 'r')
58
- else
59
- raise ArgumentError, "CalendarParser.new cannot be called with a #{src.class} type!"
60
- end
61
-
62
- @prev_line = @file.gets
63
- @prev_line.chomp! unless @prev_line.nil?
3
+ class Parser
64
4
 
65
- @@logger.debug("New Calendar Parser: #{@file.inspect}")
66
- end
5
+ attr_reader :source, :strict
67
6
 
68
- def self.escape(value)
69
- if value =~ %r{\A#{Parser::QSTR}\z|\A#{Parser::PTEXT}\z}
70
- value
7
+ def initialize(source, strict = true)
8
+ if source.respond_to? :gets
9
+ @source = source
10
+ elsif source.respond_to? :to_s
11
+ @source = StringIO.new source.to_s, 'r'
71
12
  else
72
- stripped = value.gsub '"', "'"
73
- if stripped =~ /\A#{Parser::PTEXT}\z/
74
- stripped
75
- else
76
- %|"#{stripped}"|
77
- end
78
- end
79
- end
80
-
81
- # Define next line for an IO object.
82
- # Works for strings now with StringIO
83
- def next_line
84
- line = @prev_line
85
-
86
- if line.nil?
87
- return nil
13
+ raise ArgumentError, 'Icalendar::Parser.new must be called with a String or IO object'
88
14
  end
89
-
90
- # Loop through until we get to a non-continuation line...
91
- loop do
92
- nextLine = @file.gets
93
- @@logger.debug "new_line: #{nextLine}"
94
-
95
- if !nextLine.nil?
96
- nextLine.chomp!
97
- end
98
-
99
- # If it's a continuation line, add it to the last.
100
- # If it's an empty line, drop it from the input.
101
- if( nextLine =~ /^[ \t]/ )
102
- line << nextLine[1, nextLine.size]
103
- elsif( nextLine =~ /^$/ )
104
- else
105
- @prev_line = nextLine
106
- break
107
- end
108
- end
109
- line
15
+ @strict = strict
110
16
  end
111
17
 
112
- # Parse the calendar into an object representation
113
18
  def parse
19
+ source.rewind
20
+ @data = source.gets and @data.chomp!
114
21
  calendars = []
115
-
116
- @@logger.debug "parsing..."
117
- # Outer loop for Calendar objects
118
- while (line = next_line)
119
- fields = parse_line(line)
120
-
121
- # Just iterate through until we find the beginning of a calendar object
122
- if fields[:name] == "BEGIN" and fields[:value] == "VCALENDAR"
123
- cal = parse_component Calendar.new
124
- @@logger.debug "Added parsed calendar..."
125
- calendars << cal
22
+ while (fields = next_fields)
23
+ if fields[:name] == 'begin' && fields[:value].downcase == 'vcalendar'
24
+ calendars << parse_component(Calendar.new)
126
25
  end
127
26
  end
128
-
129
27
  calendars
130
28
  end
131
29
 
132
30
  private
133
31
 
134
- # Parse a single VCALENDAR object
135
- # -- This should consist of the PRODID, VERSION, option METHOD & CALSCALE,
136
- # and then one or more calendar components: VEVENT, VTODO, VJOURNAL,
137
- # VFREEBUSY, VTIMEZONE
138
- def parse_component(component = Calendar.new)
139
- @@logger.debug "parsing new component..."
140
-
141
- while (line = next_line)
142
- fields = parse_line(line)
143
-
144
- name = fields[:name].upcase
145
-
146
- # Although properties are supposed to come before components, we should
147
- # be able to handle them in any order...
148
- if name == "END"
32
+ def parse_component(component)
33
+ while (fields = next_fields)
34
+ if fields[:name] == 'end'
149
35
  break
150
- elsif name == "BEGIN" # New component
151
- case(fields[:value])
152
- when "VEVENT" # Event
153
- component.add_component parse_component(Event.new)
154
- when "VTODO" # Todo entry
155
- component.add_component parse_component(Todo.new)
156
- when "VALARM" # Alarm sub-component for event and todo
157
- component.add_component parse_component(Alarm.new)
158
- when "VJOURNAL" # Journal entry
159
- component.add_component parse_component(Journal.new)
160
- when "VFREEBUSY" # Free/Busy section
161
- component.add_component parse_component(Freebusy.new)
162
- when "VTIMEZONE" # Timezone specification
163
- component.add_component parse_component(Timezone.new)
164
- when "STANDARD" # Standard time sub-component for timezone
165
- component.add_component parse_component(Standard.new)
166
- when "DAYLIGHT" # Daylight time sub-component for timezone
167
- component.add_component parse_component(Daylight.new)
168
- else # Uknown component type, skip to matching end
169
- until ((line = next_line) == "END:#{fields[:value]}"); end
170
- next
36
+ elsif fields[:name] == 'begin'
37
+ klass_name = fields[:value].gsub(/\AV/, '').downcase.capitalize
38
+ if Icalendar.const_defined? klass_name
39
+ component.add_component parse_component(Icalendar.const_get(klass_name).new)
40
+ elsif Icalendar::Timezone.const_defined? klass_name
41
+ component.add_component parse_component(Icalendar::Timezone.const_get(klass_name).new)
42
+ else
43
+ component.add_component parse_component(Component.new klass_name.downcase, fields[:value])
171
44
  end
172
- else # If its not a component then it should be a property
173
- params = fields[:params]
174
- value = fields[:value]
175
-
176
- # Lookup the property name to see if we have a string to
177
- # object parser for this property type.
178
- if @parsers.has_key?(name)
179
- value = @parsers[name].call(name, params, value)
45
+ else
46
+ # new property
47
+ klass = component.class.default_property_types[fields[:name]]
48
+ if !fields[:params]['value'].nil?
49
+ klass_name = fields[:params].delete('value').first
50
+ unless klass_name.upcase == klass.value_type
51
+ klass_name = klass_name.downcase.gsub(/(?:\A|-)(.)/) { |m| m[-1].upcase }
52
+ klass = Icalendar::Values.const_get klass_name if Icalendar::Values.const_defined?(klass_name)
53
+ end
180
54
  end
181
-
182
- name = name.downcase
183
-
184
- # TODO: check to see if there are any more conflicts.
185
- if name == 'class' or name == 'method' or name == 'name'
186
- name = "ip_" + name
55
+ if klass.value_type != 'RECUR' && fields[:value] =~ /(?<!\\)([,;])/
56
+ delimiter = $1
57
+ prop_value = Icalendar::Values::Array.new fields[:value].split(/(?<!\\)[;,]/),
58
+ klass,
59
+ fields[:params],
60
+ delimiter: delimiter
61
+ else
62
+ prop_value = klass.new fields[:value], fields[:params]
187
63
  end
188
-
189
- # Replace dashes with underscores
190
- name = name.gsub('-', '_')
191
-
192
- if component.multiline_property?(name)
193
- adder = "add_" + name
194
- if component.respond_to?(adder)
195
- component.send(adder, value, params)
196
- else
197
- raise(UnknownPropertyMethod, "Unknown property type: #{adder}") if strict
198
- end
64
+ prop_name = %w(class method).include?(fields[:name]) ? "ip_#{fields[:name]}" : fields[:name]
65
+ if component.class.multiple_properties.include? prop_name
66
+ component.send "append_#{prop_name}", prop_value
199
67
  else
200
- if component.respond_to?(name)
201
- component.send(name, value, params)
202
- else
203
- raise(UnknownPropertyMethod, "Unknown property type: #{name}") if strict
204
- end
68
+ component.send "#{prop_name}=", prop_value
205
69
  end
206
70
  end
207
71
  end
208
-
209
72
  component
210
73
  end
211
74
 
212
- # 1*(ALPHA / DIGIT / "=")
213
- NAME = '[-a-z0-9]+'
214
-
215
- # <"> <Any character except CTLs, DQUOTE> <">
216
- QSTR = '"[^"]*"'
217
-
218
- # Contentline
219
- LINE = "(#{NAME})((?:(?:[^:]*?)(?:#{QSTR})(?:[^:]*?))+|(?:[^:]*))\:(.*)"
220
-
221
- # *<Any character except CTLs, DQUOTE, ";", ":", ",">
222
- PTEXT = '[^";:,]*'
223
-
224
- # param-value = ptext / quoted-string
225
- PVALUE = "#{QSTR}|#{PTEXT}"
226
-
227
- # param = name "=" param-value *("," param-value)
228
- PARAM = ";(#{NAME})(=?)((?:#{PVALUE})(?:,#{PVALUE})*)"
229
-
230
- def parse_line(line)
231
- unless line =~ %r{#{LINE}}i # Case insensitive match for a valid line
232
- raise "Invalid line in calendar string!"
233
- end
234
-
235
- name = $1.upcase # The case insensitive part is upcased for easier comparison...
236
- paramslist = $2
237
- value = $3.gsub("\\;", ";").gsub("\\,", ",").gsub("\\n", "\n").gsub("\\\\", "\\")
238
-
239
- # Parse the parameters
240
- params = {}
241
- if paramslist.size > 1
242
- paramslist.scan( %r{#{PARAM}}i ) do
243
-
244
- # parameter names are case-insensitive, and multi-valued
245
- pname = $1
246
- pvals = $3
247
-
248
- # If there isn't an '=' sign then we need to do some custom
249
- # business. Defaults to 'type'
250
- if $2 == ""
251
- pvals = $1
252
- case $1
253
- when /quoted-printable/i
254
- pname = 'encoding'
255
-
256
- when /base64/i
257
- pname = 'encoding'
258
-
259
- else
260
- pname = 'type'
261
- end
262
- end
263
-
264
- # Make entries into the params dictionary where the name
265
- # is the key and the value is an array of values.
266
- unless params.key? pname
267
- params[pname] = []
268
- end
269
-
270
- # Save all the values into the array.
271
- pvals.scan( %r{(#{PVALUE})} ) do
272
- if $1.size > 0
273
- params[pname] << $1
274
- end
275
- end
75
+ def next_fields
76
+ line = @data or return nil
77
+ loop do
78
+ @data = source.gets and @data.chomp!
79
+ if @data =~ /\A[ \t].+\z/
80
+ line << @data[1, @data.size]
81
+ elsif @data !~ /\A\s*\z/
82
+ break
276
83
  end
277
84
  end
278
-
279
- {:name => name, :value => value, :params => params}
280
- end
281
-
282
- ## Following is a collection of parsing functions for various
283
- ## icalendar property value data types... First we setup
284
- ## a hash with property names pointing to methods...
285
- def setup_parsers
286
- @parsers = {}
287
-
288
- # Integer properties
289
- m = self.method(:parse_integer)
290
- @parsers["PERCENT-COMPLETE"] = m
291
- @parsers["PRIORITY"] = m
292
- @parsers["REPEAT"] = m
293
- @parsers["SEQUENCE"] = m
294
-
295
- # Dates and Times
296
- m = self.method(:parse_datetime)
297
- @parsers["COMPLETED"] = m
298
- @parsers["DTEND"] = m
299
- @parsers["DUE"] = m
300
- @parsers["DTSTART"] = m
301
- @parsers["RECURRENCE-ID"] = m
302
- @parsers["CREATED"] = m
303
- @parsers["DTSTAMP"] = m
304
- @parsers["LAST-MODIFIED"] = m
305
- @parsers["ACKNOWLEDGED"] = m
306
-
307
- m = self.method(:parse_multi_datetime)
308
- @parsers["EXDATE"] = m
309
- @parsers["RDATE"] = m
310
-
311
- # URI's
312
- m = self.method(:parse_uri)
313
- @parsers["TZURL"] = m
314
- @parsers["ATTENDEE"] = m
315
- @parsers["ORGANIZER"] = m
316
- @parsers["URL"] = m
317
-
318
- # This is a URI by default, and if its not a valid URI
319
- # it will be returned as a string which works for binary data
320
- # the other possible type.
321
- @parsers["ATTACH"] = m
322
-
323
- # GEO
324
- m = self.method(:parse_geo)
325
- @parsers["GEO"] = m
326
-
327
- #RECUR
328
- m = self.method(:parse_recur)
329
- @parsers["RRULE"] = m
330
- @parsers["EXRULE"] = m
331
-
332
- end
333
-
334
- # Booleans
335
- # NOTE: It appears that although this is a valid data type
336
- # there aren't any properties that use it... Maybe get
337
- # rid of this in the future.
338
- def parse_boolean(name, params, value)
339
- if value.upcase == "FALSE"
340
- false
341
- else
342
- true
343
- end
85
+ parse_fields line
344
86
  end
345
87
 
346
- # Dates, Date-Times & Times
347
- # NOTE: invalid dates & times will be returned as strings...
348
- def parse_datetime(name, params, value)
349
- if params["VALUE"] && params["VALUE"].first == "DATE"
350
- result = Date.parse(value)
351
- else
352
- result = DateTime.parse(value)
353
- if /Z$/ =~ value
354
- timezone = "UTC"
355
- else
356
- timezone = params["TZID"].first if params["TZID"]
357
- end
358
- result.icalendar_tzid = timezone
359
- end
360
- result
361
- rescue Exception
362
- value
363
- end
88
+ NAME = '[-a-zA-Z0-9]+'
89
+ QSTR = '"[^"]*"'
90
+ PTEXT = '[^";:,]*'
91
+ PVALUE = "(?:#{QSTR}|#{PTEXT})"
92
+ PARAM = "(#{NAME})=(#{PVALUE}(?:,#{PVALUE})*)"
93
+ VALUE = '.*'
94
+ LINE = "(?<name>#{NAME})(?<params>(?:;#{PARAM})*):(?<value>#{VALUE})"
364
95
 
365
- # Multiple valued Dates
366
- def parse_multi_datetime(name, params, value)
367
- use_date = params['VALUE'] && params['VALUE'].first == 'DATE'
368
- tzid = params['TZIID'].first if params['TZID']
369
- result = []
370
- value.split(/,/).each do |val|
371
- if use_date
372
- result << Date.parse(val)
373
- else
374
- dt = DateTime.parse val
375
- if /Z$/ =~ val
376
- timezone = 'UTC'
377
- else
378
- timezone = tzid
379
- end
380
- dt.icalendar_tzid = timezone
381
- result << dt
96
+ def parse_fields(input)
97
+ parts = %r{#{LINE}}.match(input) or raise "Invalid iCalendar input line: #{input}"
98
+ params = {}
99
+ parts[:params].scan %r{#{PARAM}} do |match|
100
+ param_name = match[0].downcase
101
+ params[param_name] ||= []
102
+ match[1].scan %r{#{PVALUE}} do |param_value|
103
+ params[param_name] << param_value.gsub(/\A"|"\z/, '') if param_value.size > 0
382
104
  end
383
105
  end
384
- result
385
- rescue Exception
386
- [value]
387
- end
388
-
389
- def parse_recur(name, params, value)
390
- [::Icalendar::RRule.new(name, params, value)]
391
- end
392
-
393
- # Durations
394
- # TODO: Need to figure out the best way to represent durations
395
- # so just returning string for now.
396
- def parse_duration(name, params, value)
397
- value
106
+ {
107
+ name: parts[:name].downcase.gsub('-', '_'),
108
+ params: params,
109
+ value: parts[:value]
110
+ }
398
111
  end
399
-
400
- # Floats
401
- # NOTE: returns 0.0 if it can't parse the value
402
- def parse_float(name, params, value)
403
- value.to_f
404
- end
405
-
406
- # Integers
407
- # NOTE: returns 0 if it can't parse the value
408
- def parse_integer(name, params, value)
409
- value.to_i
410
- end
411
-
412
- # Periods
413
- # TODO: Got to figure out how to represent periods also...
414
- def parse_period(name, params, value)
415
- value
416
- end
417
-
418
- # Calendar Address's & URI's
419
- # NOTE: invalid URI's will be returned as strings...
420
- def parse_uri(name, params, value)
421
- begin
422
- URI.parse(value)
423
- rescue Exception
424
- value
425
- end
426
- end
427
-
428
- # Geographical location (GEO)
429
- # NOTE: returns an array with two floats (long & lat)
430
- # if the parsing fails return the string
431
- def parse_geo(name, params, value)
432
- strloc = value.split(';')
433
- if strloc.size != 2
434
- return value
435
- end
436
-
437
- Geo.new(strloc[0].to_f, strloc[1].to_f)
438
- end
439
-
440
112
  end
441
113
  end