icalendar 2.4.1 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/main.yml +32 -0
  3. data/.gitignore +1 -0
  4. data/History.txt +39 -0
  5. data/{COPYING → LICENSE} +0 -0
  6. data/README.md +6 -22
  7. data/icalendar.gemspec +17 -18
  8. data/lib/icalendar/alarm.rb +1 -0
  9. data/lib/icalendar/calendar.rb +10 -0
  10. data/lib/icalendar/component.rb +14 -3
  11. data/lib/icalendar/event.rb +3 -0
  12. data/lib/icalendar/has_components.rb +17 -5
  13. data/lib/icalendar/has_properties.rb +27 -19
  14. data/lib/icalendar/journal.rb +2 -0
  15. data/lib/icalendar/marshable.rb +34 -0
  16. data/lib/icalendar/parser.rb +49 -16
  17. data/lib/icalendar/timezone.rb +68 -0
  18. data/lib/icalendar/timezone_store.rb +36 -0
  19. data/lib/icalendar/todo.rb +4 -1
  20. data/lib/icalendar/tzinfo.rb +2 -2
  21. data/lib/icalendar/values/array.rb +1 -2
  22. data/lib/icalendar/values/date.rb +2 -0
  23. data/lib/icalendar/values/date_or_date_time.rb +23 -11
  24. data/lib/icalendar/values/date_time.rb +4 -0
  25. data/lib/icalendar/values/time_with_zone.rb +23 -6
  26. data/lib/icalendar/values/uri.rb +2 -2
  27. data/lib/icalendar/values/utc_offset.rb +10 -2
  28. data/lib/icalendar/version.rb +1 -1
  29. data/lib/icalendar.rb +3 -1
  30. data/spec/alarm_spec.rb +7 -2
  31. data/spec/calendar_spec.rb +34 -0
  32. data/spec/event_spec.rb +24 -0
  33. data/spec/fixtures/bad_wrapping.ics +14 -0
  34. data/spec/fixtures/custom_component.ics +158 -0
  35. data/spec/fixtures/single_event.ics +1 -1
  36. data/spec/fixtures/single_event_bad_organizer.ics +22 -0
  37. data/spec/fixtures/single_event_organizer_parsed.ics +22 -0
  38. data/spec/fixtures/tzid_search.ics +31 -0
  39. data/spec/parser_spec.rb +31 -9
  40. data/spec/roundtrip_spec.rb +32 -9
  41. data/spec/timezone_spec.rb +48 -0
  42. data/spec/tzinfo_spec.rb +1 -1
  43. data/spec/values/date_or_date_time_spec.rb +10 -0
  44. metadata +65 -40
  45. data/.travis.yml +0 -18
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 617189d256108d77d8cbf965621d324794e5ceb6
4
- data.tar.gz: 10041e3a5a825c812dc849d6ddc2997b5fcc5776
2
+ SHA256:
3
+ metadata.gz: becd9ceba3342d0b602f1bb8a33858fb2176ceafd457a0fbca76d24ff7800dc7
4
+ data.tar.gz: f11054752ca18d026b1339e936ab30d4f1ff61219ecfd94d3f913b3de6b14106
5
5
  SHA512:
6
- metadata.gz: ae3cf91f03aec36792d2f8a90b82265a71b66971bb535c6e41018bb6c611680e49d2535a0d165535011fffc85101447d545873d1f389960a4adb95e528891b59
7
- data.tar.gz: 59bb0240f2ff661a885792bacc976b273c2b10d234bc3cb32ca5c1f26031418d4f4ac58f6971dc1a2c6957c9074408674c557b98686dee83bdc6e24123efa1d5
6
+ metadata.gz: 8262dd28d571778ecc7dd638d6df2a44e4c26bea2221183d82a644c777d100a75027c83ac83fd53c801d43cd75b9521be104111900d6653119a0593a281b8203
7
+ data.tar.gz: 1dea941db6e27188b9ab0d769a0df2335fdc2bc492a4ca6339a5e4a90e0d91f9821282aaed13b2cc4f8a4921e24a3d20f14aa34fea97c50c6326d3a1dc29f3cc
@@ -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
+ - 2.7.6
21
+ - 3.0.4
22
+ - 3.1.2
23
+
24
+ steps:
25
+ - uses: actions/checkout@v2
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/.gitignore CHANGED
@@ -6,3 +6,4 @@ pkg/
6
6
  .bundle
7
7
  coverage/
8
8
  tags
9
+ .idea
data/History.txt CHANGED
@@ -1,3 +1,42 @@
1
+ === 2.8.0 2022-07-10
2
+ * Fix compatibility with ActiveSupport 7 - Pat Allan
3
+ * Set default action of "DISPLAY" on alarms - Rikson
4
+ * Add license information to gemspec - Robert Reiz
5
+ * Support RFC7986 properties - Daniele Frisanco
6
+
7
+ === 2.7.1 2021-03-14
8
+ * Recover from bad line-wrapping code that splits in the middle of Unicode code points
9
+ * Add a verbose option to the Parser to quiet some of the chattier log entries
10
+
11
+ === 2.7.0 2020-09-12
12
+ * Handle custom component names, with and without X- prefix
13
+ * Fix Component lookup to avoid namespace collisions
14
+
15
+ === 2.6.1 2019-12-07
16
+ * Improve performance when generating large ICS files - Alex Balhatchet
17
+
18
+ === 2.6.0 2019-11-26
19
+ * Improve performance for calculating timezone offsets - Justin Howard
20
+ * Make it possible to de/serialize with Marshal - Pawel Niewiadomski
21
+ * Avoid FrozenError when running with frozen_string_literal
22
+ * Update minimum Ruby version to supported versions
23
+
24
+ === 2.5.3 2019-03-04
25
+ * Improve parsing performance - nehresma
26
+ * Support tzinfo 2.0 - Misty De Meo
27
+
28
+ === 2.5.2 2018-12-08
29
+ * Remove usage of the global TimezoneStore instance, in favor of a local variable in the parser
30
+ * Deprecate TimezoneStore class methods
31
+
32
+ === 2.5.1 2018-10-30
33
+ * Fix usage without ActiveSupport installed.
34
+
35
+ === 2.5.0 2018-09-10
36
+ * Set timezone information from VTIMEZONE components in cases that ActiveSupport can't figure it out (or isn't installed)
37
+ * Prevent rewinding the Parser IO input during parsing - Niels Laukens
38
+ * Update tested/supported ruby versions & documentation updates.
39
+
1
40
  === 2.4.1 2016-09-03
2
41
  * Fix parsing multiple calendars or components in same file - Patrick Schnetger
3
42
  * Fix multi-byte folding bug - Niels Laukens
File without changes
data/README.md CHANGED
@@ -1,28 +1,11 @@
1
1
  iCalendar -- Internet calendaring, Ruby style
2
2
  ===
3
3
 
4
- [![Build Status](https://travis-ci.org/icalendar/icalendar.svg?branch=master)](https://travis-ci.org/icalendar/icalendar)
4
+ [![Build Status](https://travis-ci.com/icalendar/icalendar.svg?branch=master)](https://travis-ci.com/icalendar/icalendar)
5
5
  [![Code Climate](https://codeclimate.com/github/icalendar/icalendar.png)](https://codeclimate.com/github/icalendar/icalendar)
6
6
 
7
7
  <http://github.com/icalendar/icalendar>
8
8
 
9
- 2.x Status
10
- ---
11
-
12
- iCalendar 2.0 is under active development, and can be followed in the
13
- [master branch](https://github.com/icalendar/icalendar/tree/master).
14
-
15
- iCalendar 1.x (currently the 1.x branch) will still survive for a
16
- while, but will only be accepting bug fixes from this point forward
17
- unless someone else wants to take over more active maintainership of
18
- the 1.x series.
19
-
20
- ### 2.0 Goals ###
21
-
22
- * Implements [RFC 5545](http://tools.ietf.org/html/rfc5545)
23
- * More obvious access to parameters and values
24
- * Cleaner & easier timezone support
25
-
26
9
  ### Upgrade from 1.x ###
27
10
 
28
11
  Better documentation is still to come, but in the meantime the changes needed to move from 1.x to 2.0 are summarized by the [diff needed to update the README](https://github.com/icalendar/icalendar/commit/bc3701e004c915a250054030a9375d1e7618857f)
@@ -92,11 +75,11 @@ event.summary.ical_params #=> {'altrep' => 'http://my.language.net', 'language'
92
75
 
93
76
  #### Support for Dates or DateTimes
94
77
 
95
- Sometimes we don't care if an event's start or end are `Date` or `DateTime` objects. For this, we can use `DateOrDateTime.new(value).call`
78
+ Sometimes we don't care if an event's start or end are `Date` or `DateTime` objects. For this, we can use `DateOrDateTime.new(value)`. Calling `.call` on the returned `DateOrDateTime` will immediately return the underlying `Date` or `DateTime` object.
96
79
 
97
80
  ```ruby
98
81
  event = cal.event do |e|
99
- e.dtstart = Icalendar::Values::DateOrDateTime.new('20140924').call
82
+ e.dtstart = Icalendar::Values::DateOrDateTime.new('20140924')
100
83
  e.dtend = Icalendar::Values::DateOrDateTime.new('20140925').call
101
84
  e.summary = 'This is an all-day event, because DateOrDateTime will return Dates'
102
85
  end
@@ -132,7 +115,7 @@ cal.event do |e|
132
115
  a.attendee = %w(mailto:me@my-domain.com mailto:me-too@my-domain.com) # one or more email recipients (required)
133
116
  a.append_attendee "mailto:me-three@my-domain.com"
134
117
  a.trigger = "-PT15M" # 15 minutes before
135
- a.append_attach Icalendar::Values::Uri.new "ftp://host.com/novo-procs/felizano.exe", "fmttype" => "application/binary" # email attachments (optional)
118
+ a.append_attach Icalendar::Values::Uri.new("ftp://host.com/novo-procs/felizano.exe", "fmttype" => "application/binary") # email attachments (optional)
136
119
  end
137
120
 
138
121
  e.alarm do |a|
@@ -237,7 +220,8 @@ end
237
220
  iCalendar has some basic support for creating VTIMEZONE blocks from timezone information pulled from `tzinfo`.
238
221
  You must require `tzinfo` support manually to take advantage.
239
222
 
240
- iCalendar has been tested and works with `tzinfo` versions 0.3 and 1.1
223
+ iCalendar has been tested and works with `tzinfo` versions 0.3, 1.x, and 2.x. The `tzinfo-data` gem may also
224
+ be required depending on your version of `tzinfo` and potentially your operating system.
241
225
 
242
226
  #### Example ####
243
227
 
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
@@ -16,11 +17,6 @@ for the generation and parsing of .ics files, which are used by a
16
17
  variety of calendaring applications.
17
18
  EOD
18
19
  s.post_install_message = <<-EOM
19
- HEADS UP! iCalendar 2.0 is not backwards-compatible with 1.x. Please see the README for the new syntax
20
-
21
- HEADS UP! icalendar 2.2.0 switches to non-strict parsing as default. Please see the README if you
22
- rely on strict parsing for information on how to enable it.
23
-
24
20
  ActiveSupport is required for TimeWithZone support, but not required for general use.
25
21
  EOM
26
22
 
@@ -29,24 +25,27 @@ ActiveSupport is required for TimeWithZone support, but not required for general
29
25
  s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename f }
30
26
  s.require_paths = ['lib']
31
27
 
32
- s.required_ruby_version = '>= 1.9.2'
28
+ s.required_ruby_version = '>= 2.4.0'
33
29
 
34
- s.add_development_dependency 'rake', '~> 10.0'
35
- s.add_development_dependency 'bundler', '~> 1.3'
30
+ s.add_dependency 'ice_cube', '~> 0.16'
36
31
 
37
- # test with both groups of tzinfo dependencies
32
+ s.add_development_dependency 'rake', '~> 13.0'
33
+ s.add_development_dependency 'bundler', '~> 2.0'
34
+
35
+ # test with all groups of tzinfo dependencies
36
+ # tzinfo 2.x
37
+ # s.add_development_dependency 'tzinfo', '~> 2.0'
38
+ # s.add_development_dependency 'tzinfo-data', '~> 1.2020'
38
39
  # tzinfo 1.x
39
- s.add_development_dependency 'tzinfo', '~> 1.1'
40
- s.add_development_dependency 'tzinfo-data', '~> 1.2014'
40
+ s.add_development_dependency 'activesupport', '~> 6.0'
41
+ s.add_development_dependency 'i18n', '~> 1.8'
42
+ s.add_development_dependency 'tzinfo', '~> 1.2'
43
+ s.add_development_dependency 'tzinfo-data', '~> 1.2020'
41
44
  # tzinfo 0.x
42
45
  # s.add_development_dependency 'tzinfo', '~> 0.3'
43
46
  # end tzinfo
44
47
 
45
- s.add_development_dependency 'activesupport', '~> 3.2'
46
- # lock i18n to < 0.7 to maintain ruby 1.9.2 compatibility
47
- s.add_development_dependency 'i18n', '< 0.7.0'
48
-
49
- s.add_development_dependency 'timecop', '~> 0.7.0'
50
- s.add_development_dependency 'rspec', '~> 3.0'
51
- s.add_development_dependency 'simplecov', '~> 0.8'
48
+ s.add_development_dependency 'timecop', '~> 0.9'
49
+ s.add_development_dependency 'rspec', '~> 3.8'
50
+ s.add_development_dependency 'simplecov', '~> 0.16'
52
51
  end
@@ -22,6 +22,7 @@ module Icalendar
22
22
 
23
23
  def initialize
24
24
  super 'alarm'
25
+ self.action = 'DISPLAY'
25
26
  end
26
27
 
27
28
  def valid?(strict = false)
@@ -5,6 +5,16 @@ module Icalendar
5
5
  required_property :prodid
6
6
  optional_single_property :calscale
7
7
  optional_single_property :ip_method
8
+ optional_property :ip_name
9
+ optional_property :description
10
+ optional_single_property :uid
11
+ optional_single_property :last_modified, Icalendar::Values::DateTime, true
12
+ optional_single_property :url, Icalendar::Values::Uri, true
13
+ optional_property :categories
14
+ optional_single_property :refresh_interval, Icalendar::Values::Duration, true
15
+ optional_single_property :source, Icalendar::Values::Uri, true
16
+ optional_single_property :color
17
+ optional_property :image, Icalendar::Values::Uri, false, true
8
18
 
9
19
  component :timezone, :tzid
10
20
  component :event
@@ -11,9 +11,10 @@ module Icalendar
11
11
  attr_accessor :parent
12
12
 
13
13
  def self.parse(source)
14
- parser = Parser.new(source)
15
- parser.component_class = self
16
- parser.parse
14
+ _parse source
15
+ rescue ArgumentError
16
+ source.rewind if source.respond_to?(:rewind)
17
+ _parse Parser.clean_bad_wrapping(source)
17
18
  end
18
19
 
19
20
  def initialize(name, ical_name = nil)
@@ -71,6 +72,8 @@ module Icalendar
71
72
  # than 75 octets, but you need to split between characters, not bytes.
72
73
  # This is challanging with Unicode composing accents, for example.
73
74
 
75
+ return long_line if long_line.bytesize <= Icalendar::MAX_LINE_LENGTH
76
+
74
77
  chars = long_line.scan(/\P{M}\p{M}*/u) # split in graphenes
75
78
  folded = ['']
76
79
  bytes = 0
@@ -99,6 +102,14 @@ module Icalendar
99
102
  end
100
103
  collection.empty? ? nil : collection.join.chomp("\r\n")
101
104
  end
105
+
106
+ class << self
107
+ private def _parse(source)
108
+ parser = Parser.new(source)
109
+ parser.component_class = self
110
+ parser.parse
111
+ end
112
+ end
102
113
  end
103
114
 
104
115
  end
@@ -12,6 +12,7 @@ module Icalendar
12
12
  mutually_exclusive_properties :dtend, :duration
13
13
 
14
14
  optional_single_property :ip_class
15
+ optional_single_property :color
15
16
  optional_single_property :created, Icalendar::Values::DateTime
16
17
  optional_single_property :description
17
18
  optional_single_property :geo, Icalendar::Values::Float
@@ -37,6 +38,8 @@ module Icalendar
37
38
  optional_property :related_to
38
39
  optional_property :resources
39
40
  optional_property :rdate, Icalendar::Values::DateTime
41
+ optional_property :conference, Icalendar::Values::Uri, false, true
42
+ optional_property :image, Icalendar::Values::Uri, false, true
40
43
 
41
44
  component :alarm, false
42
45
 
@@ -10,7 +10,7 @@ module Icalendar
10
10
  end
11
11
 
12
12
  def initialize(*args)
13
- @custom_components = Hash.new { |h, k| h[k] = [] }
13
+ @custom_components = Hash.new
14
14
  super
15
15
  end
16
16
 
@@ -21,21 +21,33 @@ module Icalendar
21
21
  c
22
22
  end
23
23
 
24
+ def add_custom_component(component_name, c)
25
+ c.parent = self
26
+ yield c if block_given?
27
+ (custom_components[component_name.downcase.gsub("-", "_")] ||= []) << c
28
+ c
29
+ end
30
+
31
+ def custom_component(component_name)
32
+ custom_components[component_name.downcase.gsub("-", "_")] || []
33
+ end
34
+
24
35
  def method_missing(method, *args, &block)
25
36
  method_name = method.to_s
26
37
  if method_name =~ /^add_(x_\w+)$/
27
38
  component_name = $1
28
39
  custom = args.first || Component.new(component_name, component_name.upcase)
29
- custom_components[component_name] << custom
30
- yield custom if block_given?
31
- custom
40
+ add_custom_component(component_name, custom, &block)
41
+ elsif method_name =~ /^x_/ && custom_component(method_name).size > 0
42
+ custom_component method_name
32
43
  else
33
44
  super
34
45
  end
35
46
  end
36
47
 
37
48
  def respond_to_missing?(method_name, include_private = false)
38
- method_name.to_s.start_with?('add_x_') || super
49
+ string_method = method_name.to_s
50
+ string_method.start_with?('add_x_') || custom_component(string_method).size > 0 || super
39
51
  end
40
52
 
41
53
  module ClassMethods
@@ -10,7 +10,7 @@ module Icalendar
10
10
  end
11
11
 
12
12
  def initialize(*args)
13
- @custom_properties = Hash.new { |h, k| h[k] = [] }
13
+ @custom_properties = Hash.new
14
14
  super
15
15
  end
16
16
 
@@ -39,15 +39,19 @@ module Icalendar
39
39
  end
40
40
 
41
41
  def custom_property(property_name)
42
- custom_properties[property_name.downcase]
42
+ custom_properties[property_name.downcase] || []
43
43
  end
44
44
 
45
45
  def append_custom_property(property_name, value)
46
46
  property_name = property_name.downcase
47
- if value.is_a? Icalendar::Value
48
- custom_properties[property_name] << value
47
+ if self.class.single_properties.include? property_name
48
+ send "#{property_name}=", value
49
+ elsif self.class.multiple_properties.include? property_name
50
+ send "append_#{property_name}", value
51
+ elsif value.is_a? Icalendar::Value
52
+ (custom_properties[property_name] ||= []) << value
49
53
  else
50
- custom_properties[property_name] << Icalendar::Values::Text.new(value)
54
+ (custom_properties[property_name] ||= []) << Icalendar::Values::Text.new(value)
51
55
  end
52
56
  end
53
57
 
@@ -100,29 +104,29 @@ module Icalendar
100
104
  def required_property(prop, klass = Icalendar::Values::Text, validator = nil)
101
105
  validator ||= ->(component, value) { !value.nil? }
102
106
  self.required_properties[prop] = validator
103
- single_property prop, klass
107
+ single_property prop, klass, false
104
108
  end
105
109
 
106
110
  def required_multi_property(prop, klass = Icalendar::Values::Text, validator = nil)
107
111
  validator ||= ->(component, value) { !value.compact.empty? }
108
112
  self.required_properties[prop] = validator
109
- multi_property prop, klass
113
+ multi_property prop, klass, false
110
114
  end
111
115
 
112
- def optional_single_property(prop, klass = Icalendar::Values::Text)
113
- single_property prop, klass
116
+ def optional_single_property(prop, klass = Icalendar::Values::Text, new_property = false)
117
+ single_property prop, klass, new_property
114
118
  end
115
119
 
116
120
  def mutually_exclusive_properties(*properties)
117
121
  self.mutex_properties << properties
118
122
  end
119
123
 
120
- def optional_property(prop, klass = Icalendar::Values::Text, suggested_single = false)
124
+ def optional_property(prop, klass = Icalendar::Values::Text, suggested_single = false, new_property = false)
121
125
  self.suggested_single_properties << prop if suggested_single
122
- multi_property prop, klass
126
+ multi_property prop, klass, new_property
123
127
  end
124
128
 
125
- def single_property(prop, klass)
129
+ def single_property(prop, klass, new_property)
126
130
  self.single_properties << prop.to_s
127
131
  self.default_property_types[prop.to_s] = klass
128
132
 
@@ -131,17 +135,17 @@ module Icalendar
131
135
  end
132
136
 
133
137
  define_method "#{prop}=" do |value|
134
- instance_variable_set "@#{prop}", map_property_value(value, klass, false)
138
+ instance_variable_set "@#{prop}", map_property_value(value, klass, false, new_property)
135
139
  end
136
140
  end
137
141
 
138
- def multi_property(prop, klass)
142
+ def multi_property(prop, klass, new_property)
139
143
  self.multiple_properties << prop.to_s
140
144
  self.default_property_types[prop.to_s] = klass
141
145
  property_var = "@#{prop}"
142
146
 
143
147
  define_method "#{prop}=" do |value|
144
- mapped = map_property_value value, klass, true
148
+ mapped = map_property_value value, klass, true, new_property
145
149
  if mapped.is_a? Icalendar::Values::Array
146
150
  instance_variable_set property_var, mapped.to_a.compact
147
151
  else
@@ -158,20 +162,24 @@ module Icalendar
158
162
  end
159
163
 
160
164
  define_method "append_#{prop}" do |value|
161
- send(prop) << map_property_value(value, klass, true)
165
+ send(prop) << map_property_value(value, klass, true, new_property)
162
166
  end
163
167
  end
164
168
  end
165
169
 
166
170
  private
167
171
 
168
- def map_property_value(value, klass, multi_valued)
172
+ def map_property_value(value, klass, multi_valued, new_property)
173
+ params = {}
174
+ if new_property
175
+ params.merge!('VALUE': klass.value_type)
176
+ end
169
177
  if value.nil? || value.is_a?(Icalendar::Value)
170
178
  value
171
179
  elsif value.is_a? ::Array
172
- Icalendar::Values::Array.new value, klass, {}, {delimiter: (multi_valued ? ',' : ';')}
180
+ Icalendar::Values::Array.new value, klass, params, {delimiter: (multi_valued ? ',' : ';')}
173
181
  else
174
- klass.new value
182
+ klass.new value, params
175
183
  end
176
184
  end
177
185
 
@@ -6,6 +6,7 @@ module Icalendar
6
6
  required_property :uid
7
7
 
8
8
  optional_single_property :ip_class
9
+ optional_single_property :color
9
10
  optional_single_property :created, Icalendar::Values::DateTime
10
11
  optional_single_property :dtstart, Icalendar::Values::DateTime
11
12
  optional_single_property :last_modified, Icalendar::Values::DateTime
@@ -27,6 +28,7 @@ module Icalendar
27
28
  optional_property :request_status
28
29
  optional_property :related_to
29
30
  optional_property :rdate, Icalendar::Values::DateTime
31
+ optional_property :image, Icalendar::Values::Uri, false, true
30
32
 
31
33
  def initialize
32
34
  super 'journal'
@@ -0,0 +1,34 @@
1
+ module Icalendar
2
+ module Marshable
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ end
6
+
7
+ def marshal_dump
8
+ instance_variables
9
+ .reject { |ivar| self.class.transient_variables.include?(ivar) }
10
+ .each_with_object({}) do |ivar, serialized|
11
+
12
+ serialized[ivar] = instance_variable_get(ivar)
13
+ end
14
+ end
15
+
16
+ def marshal_load(serialized)
17
+ serialized.each do |ivar, value|
18
+ unless self.class.transient_variables.include?(ivar)
19
+ instance_variable_set(ivar, value)
20
+ end
21
+ end
22
+ end
23
+
24
+ module ClassMethods
25
+ def transient_variables
26
+ @transient_variables ||= [:@transient_variables]
27
+ end
28
+
29
+ def transient_variable(name)
30
+ transient_variables.push(name.to_sym)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,10 +1,27 @@
1
+ require 'icalendar/timezone_store'
2
+
1
3
  module Icalendar
2
4
 
3
5
  class Parser
4
6
  attr_writer :component_class
5
- attr_reader :source, :strict
7
+ attr_reader :source, :strict, :timezone_store, :verbose
8
+
9
+ def self.clean_bad_wrapping(source)
10
+ content = if source.respond_to? :read
11
+ source.read
12
+ elsif source.respond_to? :to_s
13
+ source.to_s
14
+ else
15
+ msg = 'Icalendar::Parser.clean_bad_wrapping must be called with a String or IO object'
16
+ Icalendar.fatal msg
17
+ fail ArgumentError, msg
18
+ end
19
+ encoding = content.encoding
20
+ content.force_encoding(Encoding::ASCII_8BIT)
21
+ content.gsub(/\r?\n[ \t]/, "").force_encoding(encoding)
22
+ end
6
23
 
7
- def initialize(source, strict = false)
24
+ def initialize(source, strict = false, verbose = false)
8
25
  if source.respond_to? :gets
9
26
  @source = source
10
27
  elsif source.respond_to? :to_s
@@ -16,11 +33,11 @@ module Icalendar
16
33
  end
17
34
  read_in_data
18
35
  @strict = strict
36
+ @verbose = verbose
37
+ @timezone_store = TimezoneStore.new
19
38
  end
20
39
 
21
40
  def parse
22
- source.rewind
23
- read_in_data
24
41
  components = []
25
42
  while (fields = next_fields)
26
43
  component = component_class.new
@@ -33,7 +50,7 @@ module Icalendar
33
50
 
34
51
  def parse_property(component, fields = nil)
35
52
  fields = next_fields if fields.nil?
36
- prop_name = %w(class method).include?(fields[:name]) ? "ip_#{fields[:name]}" : fields[:name]
53
+ prop_name = %w(class method name).include?(fields[:name]) ? "ip_#{fields[:name]}" : fields[:name]
37
54
  multi_property = component.class.multiple_properties.include? prop_name
38
55
  prop_value = wrap_property_value component, fields, multi_property
39
56
  begin
@@ -48,7 +65,7 @@ module Icalendar
48
65
  Icalendar.logger.error "No method \"#{method_name}\" for component #{component}"
49
66
  raise nme
50
67
  else
51
- Icalendar.logger.warn "No method \"#{method_name}\" for component #{component}. Appending to custom."
68
+ Icalendar.logger.warn "No method \"#{method_name}\" for component #{component}. Appending to custom." if verbose?
52
69
  component.append_custom_property prop_name, prop_value
53
70
  end
54
71
  end
@@ -81,8 +98,8 @@ module Icalendar
81
98
  if !fields[:params]['value'].nil?
82
99
  klass_name = fields[:params].delete('value').first
83
100
  unless klass_name.upcase == klass.value_type
84
- klass_name = klass_name.downcase.gsub(/(?:\A|-)(.)/) { |m| m[-1].upcase }
85
- klass = Icalendar::Values.const_get klass_name if Icalendar::Values.const_defined?(klass_name)
101
+ klass_name = "Icalendar::Values::#{klass_name.downcase.gsub(/(?:\A|-)(.)/) { |m| m[-1].upcase }}"
102
+ klass = Object.const_get klass_name if Object.const_defined?(klass_name)
86
103
  end
87
104
  end
88
105
  klass
@@ -92,6 +109,10 @@ module Icalendar
92
109
  !!@strict
93
110
  end
94
111
 
112
+ def verbose?
113
+ @verbose
114
+ end
115
+
95
116
  private
96
117
 
97
118
  def component_class
@@ -101,16 +122,18 @@ module Icalendar
101
122
  def parse_component(component)
102
123
  while (fields = next_fields)
103
124
  if fields[:name] == 'end'
125
+ klass_name = fields[:value].gsub(/\AV/, '').downcase.capitalize
126
+ timezone_store.store(component) if klass_name == 'Timezone'
104
127
  break
105
128
  elsif fields[:name] == 'begin'
106
- klass_name = fields[:value].gsub(/\AV/, '').downcase.capitalize
129
+ klass_name = fields[:value].gsub(/\AV/, '').gsub("-", "_").downcase.capitalize
107
130
  Icalendar.logger.debug "Adding component #{klass_name}"
108
- if Icalendar.const_defined? klass_name
109
- component.add_component parse_component(Icalendar.const_get(klass_name).new)
110
- elsif Icalendar::Timezone.const_defined? klass_name
111
- component.add_component parse_component(Icalendar::Timezone.const_get(klass_name).new)
131
+ if Object.const_defined? "Icalendar::#{klass_name}"
132
+ component.add_component parse_component(Object.const_get("Icalendar::#{klass_name}").new)
133
+ elsif Object.const_defined? "Icalendar::Timezone::#{klass_name}"
134
+ component.add_component parse_component(Object.const_get("Icalendar::Timezone::#{klass_name}").new)
112
135
  else
113
- component.add_component parse_component(Component.new klass_name.downcase, fields[:value])
136
+ component.add_custom_component klass_name, parse_component(Component.new klass_name.downcase, fields[:value])
114
137
  end
115
138
  else
116
139
  parse_property component, fields
@@ -160,10 +183,20 @@ module Icalendar
160
183
  param_name = match[0].downcase
161
184
  params[param_name] ||= []
162
185
  match[1].scan %r{#{PVALUE}} do |param_value|
163
- params[param_name] << param_value.gsub(/\A"|"\z/, '') if param_value.size > 0
186
+ if param_value.size > 0
187
+ param_value = param_value.gsub(/\A"|"\z/, '')
188
+ params[param_name] << param_value
189
+ if param_name == 'tzid'
190
+ params['x-tz-info'] = timezone_store.retrieve param_value
191
+ end
192
+ end
164
193
  end
165
194
  end
166
- Icalendar.logger.debug "Found fields: #{parts.inspect} with params: #{params.inspect}"
195
+ # Building the string to send to the logger is expensive.
196
+ # Only do it if the logger is at the right log level.
197
+ if ::Logger::DEBUG >= Icalendar.logger.level
198
+ Icalendar.logger.debug "Found fields: #{parts.inspect} with params: #{params.inspect}"
199
+ end
167
200
  {
168
201
  name: parts[:name].downcase.gsub('-', '_'),
169
202
  params: params,