icalendar 2.7.1 → 2.10.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 (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