icalendar 2.4.1 → 2.8.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.
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,