icalendar 2.7.1 → 2.10.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +32 -0
  3. data/History.txt +22 -0
  4. data/icalendar.gemspec +12 -6
  5. data/lib/icalendar/alarm.rb +3 -0
  6. data/lib/icalendar/calendar.rb +48 -0
  7. data/lib/icalendar/component.rb +8 -2
  8. data/lib/icalendar/downcased_hash.rb +2 -0
  9. data/lib/icalendar/event.rb +6 -1
  10. data/lib/icalendar/freebusy.rb +2 -0
  11. data/lib/icalendar/has_components.rb +7 -2
  12. data/lib/icalendar/has_properties.rb +21 -15
  13. data/lib/icalendar/journal.rb +4 -0
  14. data/lib/icalendar/logger.rb +2 -0
  15. data/lib/icalendar/marshable.rb +2 -0
  16. data/lib/icalendar/parser.rb +40 -16
  17. data/lib/icalendar/timezone.rb +8 -4
  18. data/lib/icalendar/timezone_store.rb +2 -0
  19. data/lib/icalendar/todo.rb +6 -1
  20. data/lib/icalendar/tzinfo.rb +2 -0
  21. data/lib/icalendar/value.rb +11 -3
  22. data/lib/icalendar/values/binary.rb +5 -1
  23. data/lib/icalendar/values/boolean.rb +2 -0
  24. data/lib/icalendar/values/cal_address.rb +2 -0
  25. data/lib/icalendar/values/date.rb +3 -1
  26. data/lib/icalendar/values/date_time.rb +4 -2
  27. data/lib/icalendar/values/duration.rb +15 -6
  28. data/lib/icalendar/values/float.rb +2 -0
  29. data/lib/icalendar/values/helpers/active_support_time_with_zone_adapter.rb +16 -0
  30. data/lib/icalendar/values/helpers/array.rb +62 -0
  31. data/lib/icalendar/values/helpers/time_with_zone.rb +86 -0
  32. data/lib/icalendar/values/integer.rb +2 -0
  33. data/lib/icalendar/values/period.rb +5 -1
  34. data/lib/icalendar/values/recur.rb +31 -14
  35. data/lib/icalendar/values/text.rb +5 -1
  36. data/lib/icalendar/values/time.rb +4 -2
  37. data/lib/icalendar/values/uri.rb +2 -0
  38. data/lib/icalendar/values/utc_offset.rb +6 -1
  39. data/lib/icalendar/version.rb +3 -1
  40. data/lib/icalendar.rb +2 -0
  41. data/spec/alarm_spec.rb +7 -2
  42. data/spec/calendar_spec.rb +68 -0
  43. data/spec/event_spec.rb +4 -2
  44. data/spec/fixtures/reversed_timezone.ics +143 -0
  45. data/spec/parser_spec.rb +9 -0
  46. data/spec/roundtrip_spec.rb +1 -1
  47. data/spec/values/date_time_spec.rb +16 -0
  48. metadata +33 -27
  49. data/.travis.yml +0 -17
  50. data/lib/icalendar/values/active_support_time_with_zone_adapter.rb +0 -12
  51. data/lib/icalendar/values/array.rb +0 -59
  52. data/lib/icalendar/values/time_with_zone.rb +0 -47
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbeadc87a1316894e02896fd63e7f7644a1ab5bf3a542b0d5a02c59aabf2d603
4
- data.tar.gz: 3aaf09d7984fed57644d091fa76285084d9fbf3568e6078939e7887d6f44f50a
3
+ metadata.gz: 7b9f7e30ef6614ad4175a9d282d1b03b57f140f98a2962b4e0268aa015f8f942
4
+ data.tar.gz: 7ebabfddd0f3d8e8e05e1ef82d2389007c1419c727c5404d2ec2c1c09565a8bb
5
5
  SHA512:
6
- metadata.gz: e6134ff7d12c820d7ee10a316108c1d7a9db274cccd366b93a49fa491931038edd78eb37789a24962cc6370c83fd34d67508dd24b574e8d1fa235e815c1fa22d
7
- data.tar.gz: 0bad8ab7548493a133686dab3a6d36a58aeecddb8642da83cef5a2184d0d08e97dc59307fc6fdd9574935319044ccb5ffa50f9c936aa60e5637a2430e78eb446
6
+ metadata.gz: 8d3221e4e5bd2120014ae11fe80a8e0a3b36af37b10aadd18f3bc6022e296efa47ea27d09572900bb6d6419fff64054caa6709e6b14e5182d32a188d0130b7f0
7
+ data.tar.gz: 5d7d996d53a12543de0830fdb8214e2ae39df4c19c649e81cd95ceb8d30d61f1f178b7a9a4f4e37adb591609eeac7e89d930cf5ffc24951c9cbfcb0b815c2a03
@@ -0,0 +1,32 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - master
8
+ pull_request:
9
+ branches:
10
+ - main
11
+ - master
12
+
13
+ jobs:
14
+ build:
15
+ runs-on: ubuntu-latest
16
+
17
+ strategy:
18
+ matrix:
19
+ ruby:
20
+ - 3.0.6
21
+ - 3.1.4
22
+ - 3.2.2
23
+
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - name: Set up Ruby
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: ${{ matrix.ruby }}
30
+ bundler-cache: true
31
+ - name: Run rspec tests
32
+ run: bundle exec rake spec
data/History.txt CHANGED
@@ -1,3 +1,25 @@
1
+ === Unreleased
2
+
3
+ === 2.10.1 2023-12-01
4
+ * Parsing now handles VTIMEZONE blocks defined after their TZID is used in events and other components
5
+
6
+ === 2.10.0 2023-11-01
7
+ * Add changelog metadata to gemspec - Juri Hahn
8
+ * Attempt to rescue timezone info when given a nonstandard tzid with no VTIMEZONE
9
+ * Move Values classes that shouldn't be directly used into Helpers module
10
+
11
+ === 2.9.0 2023-08-11
12
+ * Always include the VALUE of Event URLs for improved compatibility - Sean Kelley
13
+ * Improved parse performance - Thomas Cannon
14
+ * Add helper methods for other Calendar method verbs
15
+ * bugfix: Require stringio before use in Parser - vwyu
16
+
17
+ === 2.8.0 2022-07-10
18
+ * Fix compatibility with ActiveSupport 7 - Pat Allan
19
+ * Set default action of "DISPLAY" on alarms - Rikson
20
+ * Add license information to gemspec - Robert Reiz
21
+ * Support RFC7986 properties - Daniele Frisanco
22
+
1
23
  === 2.7.1 2021-03-14
2
24
  * Recover from bad line-wrapping code that splits in the middle of Unicode code points
3
25
  * Add a verbose option to the Parser to quiet some of the chattier log entries
data/icalendar.gemspec CHANGED
@@ -6,6 +6,7 @@ Gem::Specification.new do |s|
6
6
 
7
7
  s.name = "icalendar"
8
8
  s.version = Icalendar::VERSION
9
+ s.licenses = ['BSD-2-Clause', 'GPL-3.0-only', 'icalendar']
9
10
 
10
11
  s.homepage = "https://github.com/icalendar/icalendar"
11
12
  s.platform = Gem::Platform::RUBY
@@ -19,6 +20,10 @@ variety of calendaring applications.
19
20
  ActiveSupport is required for TimeWithZone support, but not required for general use.
20
21
  EOM
21
22
 
23
+ s.metadata = {
24
+ 'changelog_uri' => 'https://github.com/icalendar/icalendar/blob/main/History.txt'
25
+ }
26
+
22
27
  s.files = `git ls-files`.split "\n"
23
28
  s.test_files = `git ls-files -- {test,spec,features}/*`.split "\n"
24
29
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename f }
@@ -33,13 +38,13 @@ ActiveSupport is required for TimeWithZone support, but not required for general
33
38
 
34
39
  # test with all groups of tzinfo dependencies
35
40
  # tzinfo 2.x
36
- # s.add_development_dependency 'tzinfo', '~> 2.0'
37
- # s.add_development_dependency 'tzinfo-data', '~> 1.2020'
38
- # tzinfo 1.x
39
- s.add_development_dependency 'activesupport', '~> 6.0'
40
- s.add_development_dependency 'i18n', '~> 1.8'
41
- s.add_development_dependency 'tzinfo', '~> 1.2'
41
+ s.add_development_dependency 'activesupport', '~> 7.1'
42
+ s.add_development_dependency 'tzinfo', '~> 2.0'
42
43
  s.add_development_dependency 'tzinfo-data', '~> 1.2020'
44
+ # tzinfo 1.x
45
+ # s.add_development_dependency 'activesupport', '~> 6.0'
46
+ # s.add_development_dependency 'tzinfo', '~> 1.2'
47
+ # s.add_development_dependency 'tzinfo-data', '~> 1.2020'
43
48
  # tzinfo 0.x
44
49
  # s.add_development_dependency 'tzinfo', '~> 0.3'
45
50
  # end tzinfo
@@ -47,4 +52,5 @@ ActiveSupport is required for TimeWithZone support, but not required for general
47
52
  s.add_development_dependency 'timecop', '~> 0.9'
48
53
  s.add_development_dependency 'rspec', '~> 3.8'
49
54
  s.add_development_dependency 'simplecov', '~> 0.16'
55
+ s.add_development_dependency 'byebug'
50
56
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Alarm < Component
@@ -22,6 +24,7 @@ module Icalendar
22
24
 
23
25
  def initialize
24
26
  super 'alarm'
27
+ self.action = 'DISPLAY'
25
28
  end
26
29
 
27
30
  def valid?(strict = false)
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Calendar < Component
@@ -5,6 +7,16 @@ module Icalendar
5
7
  required_property :prodid
6
8
  optional_single_property :calscale
7
9
  optional_single_property :ip_method
10
+ optional_property :ip_name
11
+ optional_property :description
12
+ optional_single_property :uid
13
+ optional_single_property :last_modified, Icalendar::Values::DateTime, true
14
+ optional_single_property :url, Icalendar::Values::Uri, true
15
+ optional_property :categories
16
+ optional_single_property :refresh_interval, Icalendar::Values::Duration, true
17
+ optional_single_property :source, Icalendar::Values::Uri, true
18
+ optional_single_property :color
19
+ optional_property :image, Icalendar::Values::Uri, false, true
8
20
 
9
21
  component :timezone, :tzid
10
22
  component :event
@@ -21,6 +33,42 @@ module Icalendar
21
33
 
22
34
  def publish
23
35
  self.ip_method = 'PUBLISH'
36
+ self
37
+ end
38
+
39
+ def request
40
+ self.ip_method = 'REQUEST'
41
+ self
42
+ end
43
+
44
+ def reply
45
+ self.ip_method = 'REPLY'
46
+ self
47
+ end
48
+
49
+ def add
50
+ self.ip_method = 'ADD'
51
+ self
52
+ end
53
+
54
+ def cancel
55
+ self.ip_method = 'CANCEL'
56
+ self
57
+ end
58
+
59
+ def refresh
60
+ self.ip_method = 'REFRESH'
61
+ self
62
+ end
63
+
64
+ def counter
65
+ self.ip_method = 'COUNTER'
66
+ self
67
+ end
68
+
69
+ def decline_counter
70
+ self.ip_method = 'DECLINECOUNTER'
71
+ self
24
72
  end
25
73
 
26
74
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'securerandom'
2
4
 
3
5
  module Icalendar
@@ -53,10 +55,14 @@ module Icalendar
53
55
  end.compact.join "\r\n"
54
56
  end
55
57
 
58
+ ICAL_PROP_NAME_GSUB_REGEX = /\Aip_/.freeze
59
+
56
60
  def ical_prop_name(prop_name)
57
- prop_name.gsub(/\Aip_/, '').gsub('_', '-').upcase
61
+ prop_name.gsub(ICAL_PROP_NAME_GSUB_REGEX, '').gsub('_', '-').upcase
58
62
  end
59
63
 
64
+ ICAL_FOLD_LONG_LINE_SCAN_REGEX = /\P{M}\p{M}*/u.freeze
65
+
60
66
  def ical_fold(long_line, indent = "\x20")
61
67
  # rfc2445 says:
62
68
  # Lines of text SHOULD NOT be longer than 75 octets, excluding the line
@@ -74,7 +80,7 @@ module Icalendar
74
80
 
75
81
  return long_line if long_line.bytesize <= Icalendar::MAX_LINE_LENGTH
76
82
 
77
- chars = long_line.scan(/\P{M}\p{M}*/u) # split in graphenes
83
+ chars = long_line.scan(ICAL_FOLD_LONG_LINE_SCAN_REGEX) # split in graphenes
78
84
  folded = ['']
79
85
  bytes = 0
80
86
  while chars.count > 0
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
4
 
3
5
  module Icalendar
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Event < Component
@@ -12,6 +14,7 @@ module Icalendar
12
14
  mutually_exclusive_properties :dtend, :duration
13
15
 
14
16
  optional_single_property :ip_class
17
+ optional_single_property :color
15
18
  optional_single_property :created, Icalendar::Values::DateTime
16
19
  optional_single_property :description
17
20
  optional_single_property :geo, Icalendar::Values::Float
@@ -23,7 +26,7 @@ module Icalendar
23
26
  optional_single_property :status
24
27
  optional_single_property :summary
25
28
  optional_single_property :transp
26
- optional_single_property :url, Icalendar::Values::Uri
29
+ optional_single_property :url, Icalendar::Values::Uri, true
27
30
  optional_single_property :recurrence_id, Icalendar::Values::DateTime
28
31
 
29
32
  optional_property :rrule, Icalendar::Values::Recur, true
@@ -37,6 +40,8 @@ module Icalendar
37
40
  optional_property :related_to
38
41
  optional_property :resources
39
42
  optional_property :rdate, Icalendar::Values::DateTime
43
+ optional_property :conference, Icalendar::Values::Uri, false, true
44
+ optional_property :image, Icalendar::Values::Uri, false, true
40
45
 
41
46
  component :alarm, false
42
47
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Freebusy < Component
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  module HasComponents
@@ -32,13 +34,16 @@ module Icalendar
32
34
  custom_components[component_name.downcase.gsub("-", "_")] || []
33
35
  end
34
36
 
37
+ METHOD_MISSING_ADD_REGEX = /^add_(x_\w+)$/.freeze
38
+ METHOD_MISSING_X_FLAG_REGEX = /^x_/.freeze
39
+
35
40
  def method_missing(method, *args, &block)
36
41
  method_name = method.to_s
37
- if method_name =~ /^add_(x_\w+)$/
42
+ if method_name =~ METHOD_MISSING_ADD_REGEX
38
43
  component_name = $1
39
44
  custom = args.first || Component.new(component_name, component_name.upcase)
40
45
  add_custom_component(component_name, custom, &block)
41
- elsif method_name =~ /^x_/ && custom_component(method_name).size > 0
46
+ elsif method_name =~ METHOD_MISSING_X_FLAG_REGEX && custom_component(method_name).size > 0
42
47
  custom_component method_name
43
48
  else
44
49
  super
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  module HasProperties
@@ -104,29 +106,29 @@ module Icalendar
104
106
  def required_property(prop, klass = Icalendar::Values::Text, validator = nil)
105
107
  validator ||= ->(component, value) { !value.nil? }
106
108
  self.required_properties[prop] = validator
107
- single_property prop, klass
109
+ single_property prop, klass, false
108
110
  end
109
111
 
110
112
  def required_multi_property(prop, klass = Icalendar::Values::Text, validator = nil)
111
113
  validator ||= ->(component, value) { !value.compact.empty? }
112
114
  self.required_properties[prop] = validator
113
- multi_property prop, klass
115
+ multi_property prop, klass, false
114
116
  end
115
117
 
116
- def optional_single_property(prop, klass = Icalendar::Values::Text)
117
- single_property prop, klass
118
+ def optional_single_property(prop, klass = Icalendar::Values::Text, new_property = false)
119
+ single_property prop, klass, new_property
118
120
  end
119
121
 
120
122
  def mutually_exclusive_properties(*properties)
121
123
  self.mutex_properties << properties
122
124
  end
123
125
 
124
- def optional_property(prop, klass = Icalendar::Values::Text, suggested_single = false)
126
+ def optional_property(prop, klass = Icalendar::Values::Text, suggested_single = false, new_property = false)
125
127
  self.suggested_single_properties << prop if suggested_single
126
- multi_property prop, klass
128
+ multi_property prop, klass, new_property
127
129
  end
128
130
 
129
- def single_property(prop, klass)
131
+ def single_property(prop, klass, new_property)
130
132
  self.single_properties << prop.to_s
131
133
  self.default_property_types[prop.to_s] = klass
132
134
 
@@ -135,18 +137,18 @@ module Icalendar
135
137
  end
136
138
 
137
139
  define_method "#{prop}=" do |value|
138
- instance_variable_set "@#{prop}", map_property_value(value, klass, false)
140
+ instance_variable_set "@#{prop}", map_property_value(value, klass, false, new_property)
139
141
  end
140
142
  end
141
143
 
142
- def multi_property(prop, klass)
144
+ def multi_property(prop, klass, new_property)
143
145
  self.multiple_properties << prop.to_s
144
146
  self.default_property_types[prop.to_s] = klass
145
147
  property_var = "@#{prop}"
146
148
 
147
149
  define_method "#{prop}=" do |value|
148
- mapped = map_property_value value, klass, true
149
- if mapped.is_a? Icalendar::Values::Array
150
+ mapped = map_property_value value, klass, true, new_property
151
+ if mapped.is_a? Icalendar::Values::Helpers::Array
150
152
  instance_variable_set property_var, mapped.to_a.compact
151
153
  else
152
154
  instance_variable_set property_var, [mapped].compact
@@ -162,20 +164,24 @@ module Icalendar
162
164
  end
163
165
 
164
166
  define_method "append_#{prop}" do |value|
165
- send(prop) << map_property_value(value, klass, true)
167
+ send(prop) << map_property_value(value, klass, true, new_property)
166
168
  end
167
169
  end
168
170
  end
169
171
 
170
172
  private
171
173
 
172
- def map_property_value(value, klass, multi_valued)
174
+ def map_property_value(value, klass, multi_valued, new_property)
175
+ params = {}
176
+ if new_property
177
+ params.merge!('VALUE': klass.value_type)
178
+ end
173
179
  if value.nil? || value.is_a?(Icalendar::Value)
174
180
  value
175
181
  elsif value.is_a? ::Array
176
- Icalendar::Values::Array.new value, klass, {}, {delimiter: (multi_valued ? ',' : ';')}
182
+ Icalendar::Values::Helpers::Array.new value, klass, params, {delimiter: (multi_valued ? ',' : ';')}
177
183
  else
178
- klass.new value
184
+ klass.new value, params
179
185
  end
180
186
  end
181
187
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Journal < Component
@@ -6,6 +8,7 @@ module Icalendar
6
8
  required_property :uid
7
9
 
8
10
  optional_single_property :ip_class
11
+ optional_single_property :color
9
12
  optional_single_property :created, Icalendar::Values::DateTime
10
13
  optional_single_property :dtstart, Icalendar::Values::DateTime
11
14
  optional_single_property :last_modified, Icalendar::Values::DateTime
@@ -27,6 +30,7 @@ module Icalendar
27
30
  optional_property :request_status
28
31
  optional_property :related_to
29
32
  optional_property :rdate, Icalendar::Values::DateTime
33
+ optional_property :image, Icalendar::Values::Uri, false, true
30
34
 
31
35
  def initialize
32
36
  super 'journal'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
4
  require 'logger'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Marshable
3
5
  def self.included(base)
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'icalendar/timezone_store'
4
+ require 'stringio'
2
5
 
3
6
  module Icalendar
4
7
 
@@ -6,6 +9,8 @@ module Icalendar
6
9
  attr_writer :component_class
7
10
  attr_reader :source, :strict, :timezone_store, :verbose
8
11
 
12
+ CLEAN_BAD_WRAPPING_GSUB_REGEX = /\r?\n[ \t]/.freeze
13
+
9
14
  def self.clean_bad_wrapping(source)
10
15
  content = if source.respond_to? :read
11
16
  source.read
@@ -18,7 +23,7 @@ module Icalendar
18
23
  end
19
24
  encoding = content.encoding
20
25
  content.force_encoding(Encoding::ASCII_8BIT)
21
- content.gsub(/\r?\n[ \t]/, "").force_encoding(encoding)
26
+ content.gsub(CLEAN_BAD_WRAPPING_GSUB_REGEX, "").force_encoding(encoding)
22
27
  end
23
28
 
24
29
  def initialize(source, strict = false, verbose = false)
@@ -50,7 +55,7 @@ module Icalendar
50
55
 
51
56
  def parse_property(component, fields = nil)
52
57
  fields = next_fields if fields.nil?
53
- prop_name = %w(class method).include?(fields[:name]) ? "ip_#{fields[:name]}" : fields[:name]
58
+ prop_name = %w(class method name).include?(fields[:name]) ? "ip_#{fields[:name]}" : fields[:name]
54
59
  multi_property = component.class.multiple_properties.include? prop_name
55
60
  prop_value = wrap_property_value component, fields, multi_property
56
61
  begin
@@ -71,11 +76,15 @@ module Icalendar
71
76
  end
72
77
  end
73
78
 
79
+ WRAP_PROPERTY_VALUE_DELIMETER_REGEX = /(?<!\\)([,;])/.freeze
80
+ WRAP_PROPERTY_VALUE_SPLIT_REGEX = /(?<!\\)[;,]/.freeze
81
+
82
+
74
83
  def wrap_property_value(component, fields, multi_property)
75
84
  klass = get_wrapper_class component, fields
76
85
  if wrap_in_array? klass, fields[:value], multi_property
77
- delimiter = fields[:value].match(/(?<!\\)([,;])/)[1]
78
- Icalendar::Values::Array.new fields[:value].split(/(?<!\\)[;,]/),
86
+ delimiter = fields[:value].match(WRAP_PROPERTY_VALUE_DELIMETER_REGEX)[1]
87
+ Icalendar::Values::Helpers::Array.new fields[:value].split(WRAP_PROPERTY_VALUE_SPLIT_REGEX),
79
88
  klass,
80
89
  fields[:params],
81
90
  delimiter: delimiter
@@ -88,17 +97,22 @@ module Icalendar
88
97
  retry
89
98
  end
90
99
 
100
+ WRAP_IN_ARRAY_REGEX_1 = /(?<!\\)[,;]/.freeze
101
+ WRAP_IN_ARRAY_REGEX_2 = /(?<!\\);/.freeze
102
+
91
103
  def wrap_in_array?(klass, value, multi_property)
92
104
  klass.value_type != 'RECUR' &&
93
- ((multi_property && value =~ /(?<!\\)[,;]/) || value =~ /(?<!\\);/)
105
+ ((multi_property && value =~ WRAP_IN_ARRAY_REGEX_1) || value =~ WRAP_IN_ARRAY_REGEX_2)
94
106
  end
95
107
 
108
+ GET_WRAPPER_CLASS_GSUB_REGEX = /(?:\A|-)(.)/.freeze
109
+
96
110
  def get_wrapper_class(component, fields)
97
111
  klass = component.class.default_property_types[fields[:name]]
98
112
  if !fields[:params]['value'].nil?
99
113
  klass_name = fields[:params].delete('value').first
100
114
  unless klass_name.upcase == klass.value_type
101
- klass_name = "Icalendar::Values::#{klass_name.downcase.gsub(/(?:\A|-)(.)/) { |m| m[-1].upcase }}"
115
+ klass_name = "Icalendar::Values::#{klass_name.downcase.gsub(GET_WRAPPER_CLASS_GSUB_REGEX) { |m| m[-1].upcase }}"
102
116
  klass = Object.const_get klass_name if Object.const_defined?(klass_name)
103
117
  end
104
118
  end
@@ -119,14 +133,16 @@ module Icalendar
119
133
  @component_class ||= Icalendar::Calendar
120
134
  end
121
135
 
136
+ PARSE_COMPONENT_KLASS_NAME_GSUB_REGEX = /\AV/.freeze
137
+
122
138
  def parse_component(component)
123
139
  while (fields = next_fields)
124
140
  if fields[:name] == 'end'
125
- klass_name = fields[:value].gsub(/\AV/, '').downcase.capitalize
141
+ klass_name = fields[:value].gsub(PARSE_COMPONENT_KLASS_NAME_GSUB_REGEX, '').downcase.capitalize
126
142
  timezone_store.store(component) if klass_name == 'Timezone'
127
143
  break
128
144
  elsif fields[:name] == 'begin'
129
- klass_name = fields[:value].gsub(/\AV/, '').gsub("-", "_").downcase.capitalize
145
+ klass_name = fields[:value].gsub(PARSE_COMPONENT_KLASS_NAME_GSUB_REGEX, '').gsub("-", "_").downcase.capitalize
130
146
  Icalendar.logger.debug "Adding component #{klass_name}"
131
147
  if Object.const_defined? "Icalendar::#{klass_name}"
132
148
  component.add_component parse_component(Object.const_get("Icalendar::#{klass_name}").new)
@@ -146,13 +162,16 @@ module Icalendar
146
162
  @data = source.gets and @data.chomp!
147
163
  end
148
164
 
165
+ NEXT_FIELDS_TAB_REGEX = /\A[ \t].+\z/.freeze
166
+ NEXT_FIELDS_WHITESPACE_REGEX = /\A\s*\z/.freeze
167
+
149
168
  def next_fields
150
169
  line = @data or return nil
151
170
  loop do
152
171
  read_in_data
153
- if @data =~ /\A[ \t].+\z/
172
+ if @data =~ NEXT_FIELDS_TAB_REGEX
154
173
  line << @data[1, @data.size]
155
- elsif @data !~ /\A\s*\z/
174
+ elsif @data !~ NEXT_FIELDS_WHITESPACE_REGEX
156
175
  break
157
176
  end
158
177
  end
@@ -167,27 +186,32 @@ module Icalendar
167
186
  VALUE = '.*'
168
187
  LINE = "(?<name>#{NAME})(?<params>(?:;#{PARAM})*):(?<value>#{VALUE})"
169
188
  BAD_LINE = "(?<name>#{NAME})(?<params>(?:;#{PARAM})*)"
189
+ LINE_REGEX = %r{#{LINE}}.freeze
190
+ BAD_LINE_REGEX = %r{#{BAD_LINE}}.freeze
191
+ PARAM_REGEX = %r{#{PARAM}}.freeze
192
+ PVALUE_REGEX = %r{#{PVALUE}}.freeze
193
+ PVALUE_GSUB_REGEX = /\A"|"\z/.freeze
170
194
 
171
195
  def parse_fields(input)
172
- if parts = %r{#{LINE}}.match(input)
196
+ if parts = LINE_REGEX.match(input)
173
197
  value = parts[:value]
174
198
  else
175
- parts = %r{#{BAD_LINE}}.match(input) unless strict?
199
+ parts = BAD_LINE_REGEX.match(input) unless strict?
176
200
  parts or fail "Invalid iCalendar input line: #{input}"
177
201
  # Non-strict and bad line so use a value of empty string
178
202
  value = ''
179
203
  end
180
204
 
181
205
  params = {}
182
- parts[:params].scan %r{#{PARAM}} do |match|
206
+ parts[:params].scan PARAM_REGEX do |match|
183
207
  param_name = match[0].downcase
184
208
  params[param_name] ||= []
185
- match[1].scan %r{#{PVALUE}} do |param_value|
209
+ match[1].scan PVALUE_REGEX do |param_value|
186
210
  if param_value.size > 0
187
- param_value = param_value.gsub(/\A"|"\z/, '')
211
+ param_value = param_value.gsub(PVALUE_GSUB_REGEX, '')
188
212
  params[param_name] << param_value
189
213
  if param_name == 'tzid'
190
- params['x-tz-info'] = timezone_store.retrieve param_value
214
+ params['x-tz-store'] = timezone_store
191
215
  end
192
216
  end
193
217
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ice_cube'
2
4
 
3
5
  module Icalendar
@@ -106,15 +108,17 @@ module Icalendar
106
108
 
107
109
  def standard_for(local)
108
110
  possible = standards.map do |std|
109
- [std.previous_occurrence(local.to_time), std]
110
- end
111
+ prev = std.previous_occurrence(local.to_time)
112
+ [prev, std] unless prev.nil?
113
+ end.compact
111
114
  possible.sort_by(&:first).last
112
115
  end
113
116
 
114
117
  def daylight_for(local)
115
118
  possible = daylights.map do |day|
116
- [day.previous_occurrence(local.to_time), day]
117
- end
119
+ prev = day.previous_occurrence(local.to_time)
120
+ [prev, day] unless prev.nil?
121
+ end.compact
118
122
  possible.sort_by(&:first).last
119
123
  end
120
124
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'delegate'
2
4
  require 'icalendar/downcased_hash'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Todo < Component
@@ -12,6 +14,7 @@ module Icalendar
12
14
  mutually_exclusive_properties :due, :duration
13
15
 
14
16
  optional_single_property :ip_class
17
+ optional_single_property :color
15
18
  optional_single_property :completed, Icalendar::Values::DateTime
16
19
  optional_single_property :created, Icalendar::Values::DateTime
17
20
  optional_single_property :description
@@ -38,6 +41,8 @@ module Icalendar
38
41
  optional_property :related_to
39
42
  optional_property :resources
40
43
  optional_property :rdate, Icalendar::Values::DateTime
44
+ optional_property :conference, Icalendar::Values::Uri, false, true
45
+ optional_property :image, Icalendar::Values::Uri, false, true
41
46
 
42
47
  component :alarm, false
43
48
 
@@ -49,4 +54,4 @@ module Icalendar
49
54
 
50
55
  end
51
56
 
52
- end
57
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  =begin
2
4
  Copyright (C) 2008 Sean Dague
3
5