icalendar 2.8.0 → 2.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/main.yml +1 -0
  3. data/History.txt +8 -0
  4. data/lib/icalendar/alarm.rb +2 -0
  5. data/lib/icalendar/calendar.rb +38 -0
  6. data/lib/icalendar/component.rb +8 -2
  7. data/lib/icalendar/downcased_hash.rb +2 -0
  8. data/lib/icalendar/event.rb +3 -1
  9. data/lib/icalendar/freebusy.rb +2 -0
  10. data/lib/icalendar/has_components.rb +7 -2
  11. data/lib/icalendar/has_properties.rb +2 -0
  12. data/lib/icalendar/journal.rb +2 -0
  13. data/lib/icalendar/logger.rb +2 -0
  14. data/lib/icalendar/marshable.rb +2 -0
  15. data/lib/icalendar/parser.rb +38 -14
  16. data/lib/icalendar/timezone.rb +2 -0
  17. data/lib/icalendar/timezone_store.rb +2 -0
  18. data/lib/icalendar/todo.rb +2 -0
  19. data/lib/icalendar/tzinfo.rb +2 -0
  20. data/lib/icalendar/value.rb +10 -2
  21. data/lib/icalendar/values/active_support_time_with_zone_adapter.rb +2 -0
  22. data/lib/icalendar/values/array.rb +2 -0
  23. data/lib/icalendar/values/binary.rb +5 -1
  24. data/lib/icalendar/values/boolean.rb +2 -0
  25. data/lib/icalendar/values/cal_address.rb +2 -0
  26. data/lib/icalendar/values/date.rb +2 -0
  27. data/lib/icalendar/values/date_time.rb +2 -0
  28. data/lib/icalendar/values/duration.rb +15 -6
  29. data/lib/icalendar/values/float.rb +2 -0
  30. data/lib/icalendar/values/integer.rb +2 -0
  31. data/lib/icalendar/values/period.rb +5 -1
  32. data/lib/icalendar/values/recur.rb +31 -14
  33. data/lib/icalendar/values/text.rb +5 -1
  34. data/lib/icalendar/values/time.rb +2 -0
  35. data/lib/icalendar/values/time_with_zone.rb +2 -0
  36. data/lib/icalendar/values/uri.rb +2 -0
  37. data/lib/icalendar/values/utc_offset.rb +6 -1
  38. data/lib/icalendar/version.rb +3 -1
  39. data/lib/icalendar.rb +2 -0
  40. data/spec/calendar_spec.rb +49 -0
  41. data/spec/event_spec.rb +4 -2
  42. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: becd9ceba3342d0b602f1bb8a33858fb2176ceafd457a0fbca76d24ff7800dc7
4
- data.tar.gz: f11054752ca18d026b1339e936ab30d4f1ff61219ecfd94d3f913b3de6b14106
3
+ metadata.gz: 0f083428b61c781e197d459b5ba375139056f75e95b1b99e596169106a57ed20
4
+ data.tar.gz: acb3cdb07b0dc052664fc67619fa6301951daf70ac83a0430a839a0bdd3a5923
5
5
  SHA512:
6
- metadata.gz: 8262dd28d571778ecc7dd638d6df2a44e4c26bea2221183d82a644c777d100a75027c83ac83fd53c801d43cd75b9521be104111900d6653119a0593a281b8203
7
- data.tar.gz: 1dea941db6e27188b9ab0d769a0df2335fdc2bc492a4ca6339a5e4a90e0d91f9821282aaed13b2cc4f8a4921e24a3d20f14aa34fea97c50c6326d3a1dc29f3cc
6
+ metadata.gz: 595032415b8465be6f662f136a56dd6f4dd8af27e888c7a7b27ad1c688b3963466e34a949daa0111a65ea89037b33ea01d8f2ea24991f5d9a3869647521b6c7c
7
+ data.tar.gz: ac464077d0c421d27b39270f1d12af23b59383e3e8351b68ec41a8385cc26670ca31b2f8dba72212e8db0996ea4a9e7c4a1b7b6a9b1b34d632169fae5a715725
@@ -20,6 +20,7 @@ jobs:
20
20
  - 2.7.6
21
21
  - 3.0.4
22
22
  - 3.1.2
23
+ - 3.2.2
23
24
 
24
25
  steps:
25
26
  - uses: actions/checkout@v2
data/History.txt CHANGED
@@ -1,3 +1,11 @@
1
+ === Unreleased
2
+
3
+ === 2.9.0 2023-08-11
4
+ * Always include the VALUE of Event URLs for improved compatibility - Sean Kelley
5
+ * Improved parse performance - Thomas Cannon
6
+ * Add helper methods for other Calendar method verbs
7
+ * bugfix: Require stringio before use in Parser - vwyu
8
+
1
9
  === 2.8.0 2022-07-10
2
10
  * Fix compatibility with ActiveSupport 7 - Pat Allan
3
11
  * Set default action of "DISPLAY" on alarms - Rikson
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Alarm < Component
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Calendar < Component
@@ -31,6 +33,42 @@ module Icalendar
31
33
 
32
34
  def publish
33
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
34
72
  end
35
73
 
36
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
@@ -24,7 +26,7 @@ module Icalendar
24
26
  optional_single_property :status
25
27
  optional_single_property :summary
26
28
  optional_single_property :transp
27
- optional_single_property :url, Icalendar::Values::Uri
29
+ optional_single_property :url, Icalendar::Values::Uri, true
28
30
  optional_single_property :recurrence_id, Icalendar::Values::DateTime
29
31
 
30
32
  optional_property :rrule, Icalendar::Values::Recur, true
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Journal < Component
@@ -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)
@@ -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::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,24 +186,29 @@ 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
214
  params['x-tz-info'] = timezone_store.retrieve param_value
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ice_cube'
2
4
 
3
5
  module Icalendar
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  =begin
2
4
  Copyright (C) 2008 Sean Dague
3
5
 
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+
1
4
  require 'delegate'
2
5
  require 'icalendar/downcased_hash'
3
6
 
@@ -31,8 +34,11 @@ module Icalendar
31
34
  end
32
35
  end
33
36
 
37
+ VALUE_TYPE_GSUB_REGEX_1 = /\A.*::/.freeze
38
+ VALUE_TYPE_GSUB_REGEX_2 = /(?<!\A)[A-Z]/.freeze
39
+
34
40
  def self.value_type
35
- name.gsub(/\A.*::/, '').gsub(/(?<!\A)[A-Z]/, '-\0').upcase
41
+ name.gsub(VALUE_TYPE_GSUB_REGEX_1, '').gsub(VALUE_TYPE_GSUB_REGEX_2, '-\0').upcase
36
42
  end
37
43
 
38
44
  def value_type
@@ -54,9 +60,11 @@ module Icalendar
54
60
  "#{name.to_s.gsub('_', '-').upcase}=#{param_value}"
55
61
  end
56
62
 
63
+ ESCAPE_PARAM_VALUE_REGEX = /[;:,]/.freeze
64
+
57
65
  def escape_param_value(value)
58
66
  v = value.to_s.gsub('"', "'")
59
- v =~ /[;:,]/ ? %("#{v}") : v
67
+ v =~ ESCAPE_PARAM_VALUE_REGEX ? %("#{v}") : v
60
68
  end
61
69
 
62
70
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
  class ActiveSupportTimeWithZoneAdapter < ActiveSupport::TimeWithZone
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'base64'
2
4
 
3
5
  module Icalendar
@@ -21,9 +23,11 @@ module Icalendar
21
23
 
22
24
  private
23
25
 
26
+ BASE_64_REGEX = /\A(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{4}|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{2}==)\z/.freeze
27
+
24
28
  def base64?
25
29
  value.is_a?(String) &&
26
- value =~ /\A(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{4}|[A-Za-z0-9+\/]{3}=|[A-Za-z0-9+\/]{2}==)\z/
30
+ value =~ BASE_64_REGEX
27
31
  end
28
32
  end
29
33
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
 
3
5
  module Icalendar
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
  require_relative 'time_with_zone'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ostruct'
2
4
 
3
5
  module Icalendar
@@ -36,14 +38,21 @@ module Icalendar
36
38
  hours > 0 || minutes > 0 || seconds > 0
37
39
  end
38
40
 
41
+ DURATION_PAST_REGEX = /\A([+-])P/.freeze
42
+ DURATION_WEEKS_REGEX = /(\d+)W/.freeze
43
+ DURATION_DAYS_REGEX = /(\d+)D/.freeze
44
+ DURATION_HOURS_REGEX = /(\d+)H/.freeze
45
+ DURATION_MINUTES_REGEX = /(\d+)M/.freeze
46
+ DURATION_SECONDS_REGEX = /(\d+)S/.freeze
47
+
39
48
  def parse_fields(value)
40
49
  {
41
- past: (value =~ /\A([+-])P/ ? $1 == '-' : false),
42
- weeks: (value =~ /(\d+)W/ ? $1.to_i : 0),
43
- days: (value =~ /(\d+)D/ ? $1.to_i : 0),
44
- hours: (value =~ /(\d+)H/ ? $1.to_i : 0),
45
- minutes: (value =~ /(\d+)M/ ? $1.to_i : 0),
46
- seconds: (value =~ /(\d+)S/ ? $1.to_i : 0)
50
+ past: (value =~ DURATION_PAST_REGEX ? $1 == '-' : false),
51
+ weeks: (value =~ DURATION_WEEKS_REGEX ? $1.to_i : 0),
52
+ days: (value =~ DURATION_DAYS_REGEX ? $1.to_i : 0),
53
+ hours: (value =~ DURATION_HOURS_REGEX ? $1.to_i : 0),
54
+ minutes: (value =~ DURATION_MINUTES_REGEX ? $1.to_i : 0),
55
+ seconds: (value =~ DURATION_SECONDS_REGEX ? $1.to_i : 0)
47
56
  }
48
57
  end
49
58
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
@@ -1,12 +1,16 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
 
4
6
  class Period < Value
5
7
 
8
+ PERIOD_LAST_PART_REGEX = /\A[+-]?P.+\z/.freeze
9
+
6
10
  def initialize(value, params = {})
7
11
  parts = value.split '/'
8
12
  period_start = Icalendar::Values::DateTime.new parts.first
9
- if parts.last =~ /\A[+-]?P.+\z/
13
+ if parts.last =~ PERIOD_LAST_PART_REGEX
10
14
  period_end = Icalendar::Values::Duration.new parts.last
11
15
  else
12
16
  period_end = Icalendar::Values::DateTime.new parts.last
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ostruct'
2
4
 
3
5
  module Icalendar
@@ -44,22 +46,37 @@ module Icalendar
44
46
 
45
47
  private
46
48
 
49
+ PARSE_FIELDS_FREQUENCY_REGEX = /FREQ=(SECONDLY|MINUTELY|HOURLY|DAILY|WEEKLY|MONTHLY|YEARLY)/i.freeze
50
+ PARSE_FIELDS_UNTIL_REGEX = /UNTIL=([^;]*)/i.freeze
51
+ PARSE_FIELDS_COUNT_REGEX = /COUNT=(\d+)/i.freeze
52
+ PARSE_FIELDS_INTERVAL_REGEX = /INTERVAL=(\d+)/i.freeze
53
+ PARSE_FIELDS_BY_SECOND_REGEX = /BYSECOND=(#{NUM_LIST})(?:;|\z)/i.freeze
54
+ PARSE_FIELDS_BY_MINUTE_REGEX = /BYMINUTE=(#{NUM_LIST})(?:;|\z)/i.freeze
55
+ PARSE_FIELDS_BY_HOUR_REGEX = /BYHOUR=(#{NUM_LIST})(?:;|\z)/i.freeze
56
+ PARSE_FIELDS_BY_DAY_REGEX = /BYDAY=(#{WEEKDAY}(?:,#{WEEKDAY})*)(?:;|\z)/i.freeze
57
+ PARSE_FIELDS_BY_MONTH_DAY_REGEX = /BYMONTHDAY=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i.freeze
58
+ PARSE_FIELDS_BY_YEAR_DAY_REGEX = /BYYEARDAY=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i.freeze
59
+ PARSE_FIELDS_BY_WEEK_NUMBER_REGEX = /BYWEEKNO=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i.freeze
60
+ PARSE_FIELDS_BY_MONTH_REGEX = /BYMONTH=(#{NUM_LIST})(?:;|\z)/i.freeze
61
+ PARSE_FIELDS_BY_SET_POSITON_REGEX = /BYSETPOS=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i.freeze
62
+ PARSE_FIELDS_BY_WEEK_START_REGEX = /WKST=(#{DAYNAME})/i.freeze
63
+
47
64
  def parse_fields(value)
48
65
  {
49
- frequency: (value =~ /FREQ=(SECONDLY|MINUTELY|HOURLY|DAILY|WEEKLY|MONTHLY|YEARLY)/i ? $1.upcase : nil),
50
- until: (value =~ /UNTIL=([^;]*)/i ? $1 : nil),
51
- count: (value =~ /COUNT=(\d+)/i ? $1.to_i : nil),
52
- interval: (value =~ /INTERVAL=(\d+)/i ? $1.to_i : nil),
53
- by_second: (value =~ /BYSECOND=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
54
- by_minute: (value =~ /BYMINUTE=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
55
- by_hour: (value =~ /BYHOUR=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
56
- by_day: (value =~ /BYDAY=(#{WEEKDAY}(?:,#{WEEKDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
57
- by_month_day: (value =~ /BYMONTHDAY=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
58
- by_year_day: (value =~ /BYYEARDAY=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
59
- by_week_number: (value =~ /BYWEEKNO=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
60
- by_month: (value =~ /BYMONTH=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
61
- by_set_position: (value =~ /BYSETPOS=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
62
- week_start: (value =~ /WKST=(#{DAYNAME})/i ? $1.upcase : nil)
66
+ frequency: (value =~ PARSE_FIELDS_FREQUENCY_REGEX ? $1.upcase : nil),
67
+ until: (value =~ PARSE_FIELDS_UNTIL_REGEX ? $1 : nil),
68
+ count: (value =~ PARSE_FIELDS_COUNT_REGEX ? $1.to_i : nil),
69
+ interval: (value =~ PARSE_FIELDS_INTERVAL_REGEX ? $1.to_i : nil),
70
+ by_second: (value =~ PARSE_FIELDS_BY_SECOND_REGEX ? $1.split(',').map { |i| i.to_i } : nil),
71
+ by_minute: (value =~ PARSE_FIELDS_BY_MINUTE_REGEX ? $1.split(',').map { |i| i.to_i } : nil),
72
+ by_hour: (value =~ PARSE_FIELDS_BY_HOUR_REGEX ? $1.split(',').map { |i| i.to_i } : nil),
73
+ by_day: (value =~ PARSE_FIELDS_BY_DAY_REGEX ? $1.split(',') : nil),
74
+ by_month_day: (value =~ PARSE_FIELDS_BY_MONTH_DAY_REGEX ? $1.split(',') : nil),
75
+ by_year_day: (value =~ PARSE_FIELDS_BY_YEAR_DAY_REGEX ? $1.split(',') : nil),
76
+ by_week_number: (value =~ PARSE_FIELDS_BY_WEEK_NUMBER_REGEX ? $1.split(',') : nil),
77
+ by_month: (value =~ PARSE_FIELDS_BY_MONTH_REGEX ? $1.split(',').map { |i| i.to_i } : nil),
78
+ by_set_position: (value =~ PARSE_FIELDS_BY_SET_POSITON_REGEX ? $1.split(',') : nil),
79
+ week_start: (value =~ PARSE_FIELDS_BY_WEEK_START_REGEX ? $1.upcase : nil)
63
80
  }
64
81
  end
65
82
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
  module Values
3
5
  class Text < Value
@@ -9,12 +11,14 @@ module Icalendar
9
11
  super value, params
10
12
  end
11
13
 
14
+ VALUE_ICAL_CARRIAGE_RETURN_GSUB_REGEX = /\r?\n/.freeze
15
+
12
16
  def value_ical
13
17
  value.dup.tap do |v|
14
18
  v.gsub!('\\') { '\\\\' }
15
19
  v.gsub!(';', '\;')
16
20
  v.gsub!(',', '\,')
17
- v.gsub!(/\r?\n/, '\n')
21
+ v.gsub!(VALUE_ICAL_CARRIAGE_RETURN_GSUB_REGEX, '\n')
18
22
  end
19
23
  end
20
24
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
  require_relative 'time_with_zone'
3
5
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'icalendar/timezone_store'
2
4
 
3
5
  begin
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'uri'
2
4
 
3
5
  module Icalendar
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ostruct'
2
4
 
3
5
  module Icalendar
@@ -36,8 +38,11 @@ module Icalendar
36
38
  hours == 0 && minutes == 0 && seconds == 0
37
39
  end
38
40
 
41
+ PARSE_FIELDS_MD_REGEX = /\A(?<behind>[+-])(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})?\z/.freeze
42
+ PARSE_FIELDS_WHITESPACE_GSUB_REGEX = /\s+/.freeze
43
+
39
44
  def parse_fields(value)
40
- md = /\A(?<behind>[+-])(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})?\z/.match value.gsub(/\s+/, '')
45
+ md = PARSE_FIELDS_MD_REGEX.match value.gsub(PARSE_FIELDS_WHITESPACE_GSUB_REGEX, '')
41
46
  {
42
47
  behind: (md[:behind] == '-'),
43
48
  hours: md[:hours].to_i,
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Icalendar
2
4
 
3
- VERSION = '2.8.0'
5
+ VERSION = '2.9.0'
4
6
 
5
7
  end
data/lib/icalendar.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'icalendar/logger'
2
4
 
3
5
  module Icalendar
@@ -190,6 +190,55 @@ END:VCALENDAR
190
190
  end
191
191
  end
192
192
 
193
+ describe '#request' do
194
+ it 'sets ip_method to "REQUEST"' do
195
+ subject.request
196
+ expect(subject.ip_method).to eq 'REQUEST'
197
+ end
198
+ end
199
+
200
+ describe '#reply' do
201
+ it 'sets ip_method to "REPLY"' do
202
+ subject.reply
203
+ expect(subject.ip_method).to eq 'REPLY'
204
+ end
205
+ end
206
+
207
+ describe '#add' do
208
+ it 'sets ip_method to "ADD"' do
209
+ subject.add
210
+ expect(subject.ip_method).to eq 'ADD'
211
+ end
212
+ end
213
+
214
+ describe '#cancel' do
215
+ it 'sets ip_method to "CANCEL"' do
216
+ subject.cancel
217
+ expect(subject.ip_method).to eq 'CANCEL'
218
+ end
219
+ end
220
+
221
+ describe '#refresh' do
222
+ it 'sets ip_method to "REFRESH"' do
223
+ subject.refresh
224
+ expect(subject.ip_method).to eq 'REFRESH'
225
+ end
226
+ end
227
+
228
+ describe '#counter' do
229
+ it 'sets ip_method to "COUNTER"' do
230
+ subject.counter
231
+ expect(subject.ip_method).to eq 'COUNTER'
232
+ end
233
+ end
234
+
235
+ describe '#decline_counter' do
236
+ it 'sets ip_method to "DECLINECOUNTER"' do
237
+ subject.decline_counter
238
+ expect(subject.ip_method).to eq 'DECLINECOUNTER'
239
+ end
240
+ end
241
+
193
242
  describe '.parse' do
194
243
  let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'bad_wrapping.ics') }
195
244
 
data/spec/event_spec.rb CHANGED
@@ -51,8 +51,8 @@ describe Icalendar::Event do
51
51
  context 'suggested single values' do
52
52
  before(:each) do
53
53
  subject.dtstart = DateTime.now
54
- subject.append_rrule double('RRule').as_null_object
55
- subject.append_rrule double('RRule').as_null_object
54
+ subject.append_rrule 'RRule'
55
+ subject.append_rrule 'RRule'
56
56
  end
57
57
 
58
58
  it 'is valid by default' do
@@ -156,6 +156,7 @@ describe Icalendar::Event do
156
156
  subject.dtend = "20131227T033000Z"
157
157
  subject.summary = 'My event, my ical, my test'
158
158
  subject.geo = [41.230896,-74.411774]
159
+ subject.url = 'https://example.com'
159
160
  subject.x_custom_property = 'customize'
160
161
  end
161
162
 
@@ -163,6 +164,7 @@ describe Icalendar::Event do
163
164
  it { expect(subject.to_ical).to include 'DTEND:20131227T033000Z' }
164
165
  it { expect(subject.to_ical).to include 'SUMMARY:My event\, my ical\, my test' }
165
166
  it { expect(subject.to_ical).to include 'X-CUSTOM-PROPERTY:customize' }
167
+ it { expect(subject.to_ical).to include 'URL;VALUE=URI:https://example.com' }
166
168
  it { expect(subject.to_ical).to include 'GEO:41.230896;-74.411774' }
167
169
 
168
170
  context 'simple organizer' do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: icalendar
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.8.0
4
+ version: 2.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ahearn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-07-11 00:00:00.000000000 Z
11
+ date: 2023-08-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ice_cube