icalendar 2.8.0 → 2.10.0
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.
- checksums.yaml +4 -4
- data/.github/workflows/main.yml +4 -4
- data/History.txt +13 -0
- data/icalendar.gemspec +4 -0
- data/lib/icalendar/alarm.rb +2 -0
- data/lib/icalendar/calendar.rb +38 -0
- data/lib/icalendar/component.rb +8 -2
- data/lib/icalendar/downcased_hash.rb +2 -0
- data/lib/icalendar/event.rb +3 -1
- data/lib/icalendar/freebusy.rb +2 -0
- data/lib/icalendar/has_components.rb +7 -2
- data/lib/icalendar/has_properties.rb +4 -2
- data/lib/icalendar/journal.rb +2 -0
- data/lib/icalendar/logger.rb +2 -0
- data/lib/icalendar/marshable.rb +2 -0
- data/lib/icalendar/parser.rb +38 -14
- data/lib/icalendar/timezone.rb +2 -0
- data/lib/icalendar/timezone_store.rb +2 -0
- data/lib/icalendar/todo.rb +2 -0
- data/lib/icalendar/tzinfo.rb +2 -0
- data/lib/icalendar/value.rb +11 -3
- data/lib/icalendar/values/binary.rb +5 -1
- data/lib/icalendar/values/boolean.rb +2 -0
- data/lib/icalendar/values/cal_address.rb +2 -0
- data/lib/icalendar/values/date.rb +2 -0
- data/lib/icalendar/values/date_time.rb +4 -2
- data/lib/icalendar/values/duration.rb +15 -6
- data/lib/icalendar/values/float.rb +2 -0
- data/lib/icalendar/values/helpers/active_support_time_with_zone_adapter.rb +16 -0
- data/lib/icalendar/values/helpers/array.rb +62 -0
- data/lib/icalendar/values/helpers/time_with_zone.rb +61 -0
- data/lib/icalendar/values/integer.rb +2 -0
- data/lib/icalendar/values/period.rb +5 -1
- data/lib/icalendar/values/recur.rb +31 -14
- data/lib/icalendar/values/text.rb +5 -1
- data/lib/icalendar/values/time.rb +4 -2
- data/lib/icalendar/values/uri.rb +2 -0
- data/lib/icalendar/values/utc_offset.rb +6 -1
- data/lib/icalendar/version.rb +3 -1
- data/lib/icalendar.rb +2 -0
- data/spec/calendar_spec.rb +49 -0
- data/spec/event_spec.rb +4 -2
- data/spec/roundtrip_spec.rb +1 -1
- data/spec/values/date_time_spec.rb +16 -0
- metadata +8 -7
- data/lib/icalendar/values/active_support_time_with_zone_adapter.rb +0 -12
- data/lib/icalendar/values/array.rb +0 -58
- data/lib/icalendar/values/time_with_zone.rb +0 -53
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a4c654b36ffe5a09f521ad3c012686f12e582ba5d380242c646334f47153360
|
|
4
|
+
data.tar.gz: 184399a11e6e8a39b18ceeef59de8a9b9f7cd581f0c7169cce5c7dca63575e3b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 61837b5d19193d80d72f37edff89290fc2e5ffa0c6b9ee500036de58b0fbd9aacb5e0e9bc9f1a0a4e1ce7a3ad494ed0147e87bf697b54523d8b799e9eaa275fa
|
|
7
|
+
data.tar.gz: 00d54cc0ab4b7fc6acf616a537b426d110ddbe65dda1d6ab8ec938ba2df64e87def99023916c6193ab25d1885a2a60e8a1706617ed5a6e0adb5cb34671dae1ec
|
data/.github/workflows/main.yml
CHANGED
data/History.txt
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
=== Unreleased
|
|
2
|
+
|
|
3
|
+
=== 2.10.0 2023-11-01
|
|
4
|
+
* Add changelog metadata to gemspec - Juri Hahn
|
|
5
|
+
* Attempt to rescue timezone info when given a nonstandard tzid with no VTIMEZONE
|
|
6
|
+
* Move Values classes that shouldn't be directly used into Helpers module
|
|
7
|
+
|
|
8
|
+
=== 2.9.0 2023-08-11
|
|
9
|
+
* Always include the VALUE of Event URLs for improved compatibility - Sean Kelley
|
|
10
|
+
* Improved parse performance - Thomas Cannon
|
|
11
|
+
* Add helper methods for other Calendar method verbs
|
|
12
|
+
* bugfix: Require stringio before use in Parser - vwyu
|
|
13
|
+
|
|
1
14
|
=== 2.8.0 2022-07-10
|
|
2
15
|
* Fix compatibility with ActiveSupport 7 - Pat Allan
|
|
3
16
|
* Set default action of "DISPLAY" on alarms - Rikson
|
data/icalendar.gemspec
CHANGED
|
@@ -20,6 +20,10 @@ variety of calendaring applications.
|
|
|
20
20
|
ActiveSupport is required for TimeWithZone support, but not required for general use.
|
|
21
21
|
EOM
|
|
22
22
|
|
|
23
|
+
s.metadata = {
|
|
24
|
+
'changelog_uri' => 'https://github.com/icalendar/icalendar/blob/main/History.txt'
|
|
25
|
+
}
|
|
26
|
+
|
|
23
27
|
s.files = `git ls-files`.split "\n"
|
|
24
28
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split "\n"
|
|
25
29
|
s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename f }
|
data/lib/icalendar/alarm.rb
CHANGED
data/lib/icalendar/calendar.rb
CHANGED
|
@@ -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
|
data/lib/icalendar/component.rb
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
data/lib/icalendar/event.rb
CHANGED
|
@@ -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
|
data/lib/icalendar/freebusy.rb
CHANGED
|
@@ -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 =~
|
|
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 =~
|
|
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
|
|
@@ -146,7 +148,7 @@ module Icalendar
|
|
|
146
148
|
|
|
147
149
|
define_method "#{prop}=" do |value|
|
|
148
150
|
mapped = map_property_value value, klass, true, new_property
|
|
149
|
-
if mapped.is_a? Icalendar::Values::Array
|
|
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
|
|
@@ -177,7 +179,7 @@ module Icalendar
|
|
|
177
179
|
if value.nil? || value.is_a?(Icalendar::Value)
|
|
178
180
|
value
|
|
179
181
|
elsif value.is_a? ::Array
|
|
180
|
-
Icalendar::Values::Array.new value, klass, params, {delimiter: (multi_valued ? ',' : ';')}
|
|
182
|
+
Icalendar::Values::Helpers::Array.new value, klass, params, {delimiter: (multi_valued ? ',' : ';')}
|
|
181
183
|
else
|
|
182
184
|
klass.new value, params
|
|
183
185
|
end
|
data/lib/icalendar/journal.rb
CHANGED
data/lib/icalendar/logger.rb
CHANGED
data/lib/icalendar/marshable.rb
CHANGED
data/lib/icalendar/parser.rb
CHANGED
|
@@ -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(
|
|
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(
|
|
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 =~
|
|
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(
|
|
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(
|
|
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(
|
|
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 =~
|
|
172
|
+
if @data =~ NEXT_FIELDS_TAB_REGEX
|
|
154
173
|
line << @data[1, @data.size]
|
|
155
|
-
elsif @data !~
|
|
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 =
|
|
196
|
+
if parts = LINE_REGEX.match(input)
|
|
173
197
|
value = parts[:value]
|
|
174
198
|
else
|
|
175
|
-
parts =
|
|
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
|
|
206
|
+
parts[:params].scan PARAM_REGEX do |match|
|
|
183
207
|
param_name = match[0].downcase
|
|
184
208
|
params[param_name] ||= []
|
|
185
|
-
match[1].scan
|
|
209
|
+
match[1].scan PVALUE_REGEX do |param_value|
|
|
186
210
|
if param_value.size > 0
|
|
187
|
-
param_value = param_value.gsub(
|
|
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
|
data/lib/icalendar/timezone.rb
CHANGED
data/lib/icalendar/todo.rb
CHANGED
data/lib/icalendar/tzinfo.rb
CHANGED
data/lib/icalendar/value.rb
CHANGED
|
@@ -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(
|
|
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 =~
|
|
67
|
+
v =~ ESCAPE_PARAM_VALUE_REGEX ? %("#{v}") : v
|
|
60
68
|
end
|
|
61
69
|
|
|
62
70
|
end
|
|
@@ -64,7 +72,7 @@ module Icalendar
|
|
|
64
72
|
end
|
|
65
73
|
|
|
66
74
|
# helpers; not actual iCalendar value type
|
|
67
|
-
require_relative 'values/array'
|
|
75
|
+
require_relative 'values/helpers/array'
|
|
68
76
|
require_relative 'values/date_or_date_time'
|
|
69
77
|
|
|
70
78
|
# iCalendar value types
|
|
@@ -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 =~
|
|
30
|
+
value =~ BASE_64_REGEX
|
|
27
31
|
end
|
|
28
32
|
end
|
|
29
33
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'date'
|
|
2
|
-
require_relative 'time_with_zone'
|
|
4
|
+
require_relative 'helpers/time_with_zone'
|
|
3
5
|
|
|
4
6
|
module Icalendar
|
|
5
7
|
module Values
|
|
6
8
|
|
|
7
9
|
class DateTime < Value
|
|
8
|
-
include TimeWithZone
|
|
10
|
+
include Helpers::TimeWithZone
|
|
9
11
|
|
|
10
12
|
FORMAT = '%Y%m%dT%H%M%S'
|
|
11
13
|
|
|
@@ -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 =~
|
|
42
|
-
weeks: (value =~
|
|
43
|
-
days: (value =~
|
|
44
|
-
hours: (value =~
|
|
45
|
-
minutes: (value =~
|
|
46
|
-
seconds: (value =~
|
|
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
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Icalendar
|
|
4
|
+
module Values
|
|
5
|
+
module Helpers
|
|
6
|
+
class ActiveSupportTimeWithZoneAdapter < ActiveSupport::TimeWithZone
|
|
7
|
+
# ActiveSupport::TimeWithZone implements a #to_a method that will cause
|
|
8
|
+
# unexpected behavior in components with multi_property DateTime
|
|
9
|
+
# properties when the setters for those properties are invoked with an
|
|
10
|
+
# Icalendar::Values::DateTime that is delegating for an
|
|
11
|
+
# ActiveSupport::TimeWithZone. To avoid this behavior, undefine #to_a.
|
|
12
|
+
undef_method :to_a
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Icalendar
|
|
4
|
+
module Values
|
|
5
|
+
module Helpers
|
|
6
|
+
|
|
7
|
+
class Array < Value
|
|
8
|
+
|
|
9
|
+
attr_reader :value_delimiter
|
|
10
|
+
|
|
11
|
+
def initialize(value, klass, params = {}, options = {})
|
|
12
|
+
@value_delimiter = options[:delimiter] || ','
|
|
13
|
+
mapped = if value.is_a? ::Array
|
|
14
|
+
value.map do |v|
|
|
15
|
+
if v.is_a? Icalendar::Values::Helpers::Array
|
|
16
|
+
Icalendar::Values::Helpers::Array.new v.value, klass, v.ical_params, delimiter: v.value_delimiter
|
|
17
|
+
elsif v.is_a? ::Array
|
|
18
|
+
Icalendar::Values::Helpers::Array.new v, klass, params, delimiter: value_delimiter
|
|
19
|
+
elsif v.is_a? Icalendar::Value
|
|
20
|
+
v
|
|
21
|
+
else
|
|
22
|
+
klass.new v, params
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
else
|
|
26
|
+
[klass.new(value, params)]
|
|
27
|
+
end
|
|
28
|
+
super mapped
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def params_ical
|
|
32
|
+
value.each do |v|
|
|
33
|
+
ical_params.merge! v.ical_params
|
|
34
|
+
end
|
|
35
|
+
super
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def value_ical
|
|
39
|
+
value.map do |v|
|
|
40
|
+
v.value_ical
|
|
41
|
+
end.join value_delimiter
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def valid?
|
|
45
|
+
klass = value.first.class
|
|
46
|
+
!value.all? { |v| v.class == klass }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def value_type
|
|
50
|
+
value.first.value_type
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def needs_value_type?(default_type)
|
|
56
|
+
value.first.class != default_type
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require 'active_support/time'
|
|
5
|
+
|
|
6
|
+
if defined?(ActiveSupport::TimeWithZone)
|
|
7
|
+
require_relative 'active_support_time_with_zone_adapter'
|
|
8
|
+
end
|
|
9
|
+
rescue NameError
|
|
10
|
+
# ActiveSupport v7+ needs the base require to be run first before loading
|
|
11
|
+
# specific parts of it.
|
|
12
|
+
# https://guides.rubyonrails.org/active_support_core_extensions.html#stand-alone-active-support
|
|
13
|
+
require 'active_support'
|
|
14
|
+
retry
|
|
15
|
+
rescue LoadError
|
|
16
|
+
# tis ok, just a bit less fancy
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module Icalendar
|
|
20
|
+
module Values
|
|
21
|
+
module Helpers
|
|
22
|
+
module TimeWithZone
|
|
23
|
+
attr_reader :tz_utc
|
|
24
|
+
|
|
25
|
+
def initialize(value, params = {})
|
|
26
|
+
params = Icalendar::DowncasedHash(params)
|
|
27
|
+
@tz_utc = params['tzid'] == 'UTC'
|
|
28
|
+
x_tz_info = params.delete 'x-tz-info'
|
|
29
|
+
|
|
30
|
+
offset_value = unless params['tzid'].nil?
|
|
31
|
+
tzid = params['tzid'].is_a?(::Array) ? params['tzid'].first : params['tzid']
|
|
32
|
+
support_classes_defined = defined?(ActiveSupport::TimeZone) && defined?(ActiveSupportTimeWithZoneAdapter)
|
|
33
|
+
if support_classes_defined && (tz = ActiveSupport::TimeZone[tzid])
|
|
34
|
+
# plan a - use ActiveSupport::TimeWithZone
|
|
35
|
+
ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
|
|
36
|
+
elsif !x_tz_info.nil?
|
|
37
|
+
# plan b - use definition from provided `VTIMEZONE`
|
|
38
|
+
offset = x_tz_info.offset_for_local(value).to_s
|
|
39
|
+
if value.respond_to?(:change)
|
|
40
|
+
value.change offset: offset
|
|
41
|
+
else
|
|
42
|
+
::Time.new value.year, value.month, value.day, value.hour, value.min, value.sec, offset
|
|
43
|
+
end
|
|
44
|
+
elsif support_classes_defined && (tz = ActiveSupport::TimeZone[tzid.split.first])
|
|
45
|
+
# plan c - try to find an ActiveSupport::TimeWithZone based on the first word of the tzid
|
|
46
|
+
params['tzid'] = [tz.tzinfo.name]
|
|
47
|
+
ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
|
|
48
|
+
end
|
|
49
|
+
# plan d - just ignore the tzid
|
|
50
|
+
end
|
|
51
|
+
super((offset_value || value), params)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def params_ical
|
|
55
|
+
ical_params.delete 'tzid' if tz_utc
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -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 =~
|
|
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 =~
|
|
50
|
-
until: (value =~
|
|
51
|
-
count: (value =~
|
|
52
|
-
interval: (value =~
|
|
53
|
-
by_second: (value =~
|
|
54
|
-
by_minute: (value =~
|
|
55
|
-
by_hour: (value =~
|
|
56
|
-
by_day: (value =~
|
|
57
|
-
by_month_day: (value =~
|
|
58
|
-
by_year_day: (value =~
|
|
59
|
-
by_week_number: (value =~
|
|
60
|
-
by_month: (value =~
|
|
61
|
-
by_set_position: (value =~
|
|
62
|
-
week_start: (value =~
|
|
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!(
|
|
21
|
+
v.gsub!(VALUE_ICAL_CARRIAGE_RETURN_GSUB_REGEX, '\n')
|
|
18
22
|
end
|
|
19
23
|
end
|
|
20
24
|
end
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'date'
|
|
2
|
-
require_relative 'time_with_zone'
|
|
4
|
+
require_relative 'helpers/time_with_zone'
|
|
3
5
|
|
|
4
6
|
module Icalendar
|
|
5
7
|
module Values
|
|
6
8
|
|
|
7
9
|
class Time < Value
|
|
8
|
-
include TimeWithZone
|
|
10
|
+
include Helpers::TimeWithZone
|
|
9
11
|
|
|
10
12
|
FORMAT = '%H%M%S'
|
|
11
13
|
|
data/lib/icalendar/values/uri.rb
CHANGED
|
@@ -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 =
|
|
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,
|
data/lib/icalendar/version.rb
CHANGED
data/lib/icalendar.rb
CHANGED
data/spec/calendar_spec.rb
CHANGED
|
@@ -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
|
|
55
|
-
subject.append_rrule
|
|
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
|
data/spec/roundtrip_spec.rb
CHANGED
|
@@ -14,7 +14,7 @@ describe Icalendar do
|
|
|
14
14
|
event = Icalendar::Event.new
|
|
15
15
|
parsed = Icalendar::Calendar.parse(source).first
|
|
16
16
|
event.rdate = parsed.events.first.rdate
|
|
17
|
-
expect(event.rdate.first).to be_kind_of Icalendar::Values::Array
|
|
17
|
+
expect(event.rdate.first).to be_kind_of Icalendar::Values::Helpers::Array
|
|
18
18
|
expect(event.rdate.first.params_ical).to eq ";TZID=US-Mountain"
|
|
19
19
|
end
|
|
20
20
|
end
|
|
@@ -27,6 +27,22 @@ describe Icalendar::Values::DateTime do
|
|
|
27
27
|
expect(subject.value.utc.hour).to eq 23
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
|
+
|
|
31
|
+
context 'nonstandard format tzid local time' do
|
|
32
|
+
let(:value) { '20230901T230404' }
|
|
33
|
+
let(:params) { {'tzid' => 'Singapore Standard Time'} }
|
|
34
|
+
|
|
35
|
+
it 'parses the value as local time' do
|
|
36
|
+
expect(subject.value.hour).to eq 23
|
|
37
|
+
expect(subject.value.utc_offset).to eq 28800
|
|
38
|
+
expect(subject.value.utc.hour).to eq 15
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'updates the tzid' do
|
|
42
|
+
# use an array because that's how output from the parser will end up
|
|
43
|
+
expect(subject.ical_params['tzid']).to eq ['Asia/Singapore']
|
|
44
|
+
end
|
|
45
|
+
end
|
|
30
46
|
end
|
|
31
47
|
|
|
32
48
|
else
|
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.
|
|
4
|
+
version: 2.10.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:
|
|
11
|
+
date: 2023-11-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ice_cube
|
|
@@ -187,8 +187,6 @@ files:
|
|
|
187
187
|
- lib/icalendar/todo.rb
|
|
188
188
|
- lib/icalendar/tzinfo.rb
|
|
189
189
|
- lib/icalendar/value.rb
|
|
190
|
-
- lib/icalendar/values/active_support_time_with_zone_adapter.rb
|
|
191
|
-
- lib/icalendar/values/array.rb
|
|
192
190
|
- lib/icalendar/values/binary.rb
|
|
193
191
|
- lib/icalendar/values/boolean.rb
|
|
194
192
|
- lib/icalendar/values/cal_address.rb
|
|
@@ -197,12 +195,14 @@ files:
|
|
|
197
195
|
- lib/icalendar/values/date_time.rb
|
|
198
196
|
- lib/icalendar/values/duration.rb
|
|
199
197
|
- lib/icalendar/values/float.rb
|
|
198
|
+
- lib/icalendar/values/helpers/active_support_time_with_zone_adapter.rb
|
|
199
|
+
- lib/icalendar/values/helpers/array.rb
|
|
200
|
+
- lib/icalendar/values/helpers/time_with_zone.rb
|
|
200
201
|
- lib/icalendar/values/integer.rb
|
|
201
202
|
- lib/icalendar/values/period.rb
|
|
202
203
|
- lib/icalendar/values/recur.rb
|
|
203
204
|
- lib/icalendar/values/text.rb
|
|
204
205
|
- lib/icalendar/values/time.rb
|
|
205
|
-
- lib/icalendar/values/time_with_zone.rb
|
|
206
206
|
- lib/icalendar/values/uri.rb
|
|
207
207
|
- lib/icalendar/values/utc_offset.rb
|
|
208
208
|
- lib/icalendar/version.rb
|
|
@@ -247,7 +247,8 @@ licenses:
|
|
|
247
247
|
- BSD-2-Clause
|
|
248
248
|
- GPL-3.0-only
|
|
249
249
|
- icalendar
|
|
250
|
-
metadata:
|
|
250
|
+
metadata:
|
|
251
|
+
changelog_uri: https://github.com/icalendar/icalendar/blob/main/History.txt
|
|
251
252
|
post_install_message: 'ActiveSupport is required for TimeWithZone support, but not
|
|
252
253
|
required for general use.
|
|
253
254
|
|
|
@@ -266,7 +267,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
266
267
|
- !ruby/object:Gem::Version
|
|
267
268
|
version: '0'
|
|
268
269
|
requirements: []
|
|
269
|
-
rubygems_version: 3.
|
|
270
|
+
rubygems_version: 3.4.10
|
|
270
271
|
signing_key:
|
|
271
272
|
specification_version: 4
|
|
272
273
|
summary: A ruby implementation of the iCalendar specification (RFC-5545).
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
module Icalendar
|
|
2
|
-
module Values
|
|
3
|
-
class ActiveSupportTimeWithZoneAdapter < ActiveSupport::TimeWithZone
|
|
4
|
-
# ActiveSupport::TimeWithZone implements a #to_a method that will cause
|
|
5
|
-
# unexpected behavior in components with multi_property DateTime
|
|
6
|
-
# properties when the setters for those properties are invoked with an
|
|
7
|
-
# Icalendar::Values::DateTime that is delegating for an
|
|
8
|
-
# ActiveSupport::TimeWithZone. To avoid this behavior, undefine #to_a.
|
|
9
|
-
undef_method :to_a
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
module Icalendar
|
|
2
|
-
module Values
|
|
3
|
-
|
|
4
|
-
class Array < Value
|
|
5
|
-
|
|
6
|
-
attr_reader :value_delimiter
|
|
7
|
-
|
|
8
|
-
def initialize(value, klass, params = {}, options = {})
|
|
9
|
-
@value_delimiter = options[:delimiter] || ','
|
|
10
|
-
mapped = if value.is_a? ::Array
|
|
11
|
-
value.map do |v|
|
|
12
|
-
if v.is_a? Icalendar::Values::Array
|
|
13
|
-
Icalendar::Values::Array.new v.value, klass, v.ical_params, delimiter: v.value_delimiter
|
|
14
|
-
elsif v.is_a? ::Array
|
|
15
|
-
Icalendar::Values::Array.new v, klass, params, delimiter: value_delimiter
|
|
16
|
-
elsif v.is_a? Icalendar::Value
|
|
17
|
-
v
|
|
18
|
-
else
|
|
19
|
-
klass.new v, params
|
|
20
|
-
end
|
|
21
|
-
end
|
|
22
|
-
else
|
|
23
|
-
[klass.new(value, params)]
|
|
24
|
-
end
|
|
25
|
-
super mapped
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def params_ical
|
|
29
|
-
value.each do |v|
|
|
30
|
-
ical_params.merge! v.ical_params
|
|
31
|
-
end
|
|
32
|
-
super
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def value_ical
|
|
36
|
-
value.map do |v|
|
|
37
|
-
v.value_ical
|
|
38
|
-
end.join value_delimiter
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def valid?
|
|
42
|
-
klass = value.first.class
|
|
43
|
-
!value.all? { |v| v.class == klass }
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def value_type
|
|
47
|
-
value.first.value_type
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
|
|
52
|
-
def needs_value_type?(default_type)
|
|
53
|
-
value.first.class != default_type
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
end
|
|
58
|
-
end
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
require 'icalendar/timezone_store'
|
|
2
|
-
|
|
3
|
-
begin
|
|
4
|
-
require 'active_support/time'
|
|
5
|
-
|
|
6
|
-
if defined?(ActiveSupport::TimeWithZone)
|
|
7
|
-
require 'icalendar/values/active_support_time_with_zone_adapter'
|
|
8
|
-
end
|
|
9
|
-
rescue NameError
|
|
10
|
-
# ActiveSupport v7+ needs the base require to be run first before loading
|
|
11
|
-
# specific parts of it.
|
|
12
|
-
# https://guides.rubyonrails.org/active_support_core_extensions.html#stand-alone-active-support
|
|
13
|
-
require 'active_support'
|
|
14
|
-
retry
|
|
15
|
-
rescue LoadError
|
|
16
|
-
# tis ok, just a bit less fancy
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
module Icalendar
|
|
20
|
-
module Values
|
|
21
|
-
module TimeWithZone
|
|
22
|
-
attr_reader :tz_utc
|
|
23
|
-
|
|
24
|
-
def initialize(value, params = {})
|
|
25
|
-
params = Icalendar::DowncasedHash(params)
|
|
26
|
-
@tz_utc = params['tzid'] == 'UTC'
|
|
27
|
-
x_tz_info = params.delete 'x-tz-info'
|
|
28
|
-
|
|
29
|
-
offset_value = unless params['tzid'].nil?
|
|
30
|
-
tzid = params['tzid'].is_a?(::Array) ? params['tzid'].first : params['tzid']
|
|
31
|
-
if defined?(ActiveSupport::TimeZone) &&
|
|
32
|
-
defined?(ActiveSupportTimeWithZoneAdapter) &&
|
|
33
|
-
(tz = ActiveSupport::TimeZone[tzid])
|
|
34
|
-
ActiveSupportTimeWithZoneAdapter.new(nil, tz, value)
|
|
35
|
-
elsif !x_tz_info.nil?
|
|
36
|
-
offset = x_tz_info.offset_for_local(value).to_s
|
|
37
|
-
if value.respond_to?(:change)
|
|
38
|
-
value.change offset: offset
|
|
39
|
-
else
|
|
40
|
-
::Time.new value.year, value.month, value.day, value.hour, value.min, value.sec, offset
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
super((offset_value || value), params)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def params_ical
|
|
48
|
-
ical_params.delete 'tzid' if tz_utc
|
|
49
|
-
super
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|