icalendar 1.5.4 → 2.0.0.beta.1

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