icalendar 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 7f4b5d7252174baceb4b7e37a64286aaa38523ff
4
- data.tar.gz: bfa01a3b7d32a7216f82a63f3b216ddfc2bc8745
3
+ metadata.gz: 44c08a1f244a35efd4affc4705028dfe9798f707
4
+ data.tar.gz: f67a4772936897ed6fed5efd4d3d6d8f6c564ac9
5
5
  SHA512:
6
- metadata.gz: a79833843dcf6a77bc4d4135e25b1bdaa436db93c4195ef76a40e3e8f00fc01dbf28c24871bac057bf362eea201b066b655a2fe16215da5a32576221d3393658
7
- data.tar.gz: a2a2d04e8784be3f057cc9f23adcf45ec365282d035cb8655701fe6d08c531381dc7c0fceb8fb42948f8226042d20b1f7cee56a51e3d0ac46dcf1500cd387cfa
6
+ metadata.gz: 336fc24e516f61e887be407e55c5670475c965770a13e42be3b97fc1843cb02f5a21f40cba4aa76887f868c51637c0d9510548748f4dce34d8ff0d62579d0529
7
+ data.tar.gz: aeb52da8e30cea70b4faaa2b931b827e19952837f85c2403959859702fa2b80213a0ef4d05d0c7d45ac0ab99bc90b7fa6f96e36e5bc7d1c163d1f6ac4dca06ee
data/History.txt CHANGED
@@ -1,3 +1,7 @@
1
+ === 1.5.1 2014-02-27
2
+ * Check for dtend existence before setting timezone - Jonas Grau
3
+ * Clean up and refactor components - Kasper Timm Hansen
4
+
1
5
  === 1.5.0 2013-12-06
2
6
  * Support for custom x- properties - Jake Craige
3
7
 
data/README.md CHANGED
@@ -6,6 +6,24 @@ iCalendar -- Internet calendaring, Ruby style
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
+ [2.0beta branch](https://github.com/icalendar/icalendar/tree/2.0beta).
14
+
15
+ iCalendar 1.x (currently the master 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
27
  DESCRIPTION
10
28
  ---
11
29
 
@@ -92,7 +110,7 @@ ALARMS
92
110
 
93
111
  ### Within an event ###
94
112
 
95
- cal.event.do
113
+ cal.event do
96
114
  # ...other event properties
97
115
  alarm do
98
116
  action "EMAIL"
@@ -186,6 +204,31 @@ TIMEZONES
186
204
  # END:STANDARD
187
205
  # END:VTIMEZONE
188
206
 
207
+ iCalendar has some basic support for creating VTIMEZONE blocks from timezone information pulled from `tzinfo`. You must require `tzinfo` support manually to take advantage, and iCalendar only supports `tzinfo` with versions `~> 0.3`
208
+
209
+ #### Example ####
210
+
211
+ require 'tzinfo'
212
+ require 'icalendar/tzinfo'
213
+
214
+ cal = Calendar.new
215
+
216
+ event_start = DateTime.new 2008, 12, 29, 8, 0, 0
217
+ event_end = DateTime.new 2008, 12, 29, 11, 0, 0
218
+
219
+ tzid = "America/Chicago"
220
+ tz = TZInfo::Timezone.get tzid
221
+ timezone = tz.ical_timezone event_start
222
+ cal.add timezone
223
+
224
+ cal.event do
225
+ dtstart event_start.tap { |d| d.ical_params = {'TZID' => tzid} }
226
+ dtend event_end.tap { |d| d.ical_params = {'TZID' => tzid} }
227
+ summary "Meeting with the man."
228
+ description "Have a long lunch meeting and decide nothing..."
229
+ end
230
+
231
+
189
232
  Unicode
190
233
  ---
191
234
 
data/Rakefile CHANGED
@@ -8,3 +8,11 @@ Rake::TestTask.new do |t|
8
8
  end
9
9
 
10
10
  task default: [:test, :build]
11
+
12
+ task :console do
13
+ require 'irb'
14
+ require 'irb/completion'
15
+ require 'icalendar'
16
+ ARGV.clear
17
+ IRB.start
18
+ end
@@ -11,7 +11,7 @@ require 'logger'
11
11
 
12
12
  module Icalendar #:nodoc:
13
13
 
14
- VERSION = '1.5.0'
14
+ VERSION = '1.5.1'
15
15
 
16
16
  # A simple error class to differentiate iCalendar library exceptions
17
17
  # from ruby language exceptions or others.
@@ -25,88 +25,60 @@ module Icalendar
25
25
  self.version = "2.0" # Version of the specification
26
26
  end
27
27
 
28
- def print_component
29
- "BEGIN:#{@name.upcase}\r\n" +
30
- "VERSION:#{version}\r\n" +
31
-
32
- # Then the properties
33
- print_properties(@properties.select { |k,v| k != 'version' }) +
34
-
35
- # sub components
36
- yield +
28
+ def print_headers
29
+ "VERSION:#{version}\r\n"
30
+ end
37
31
 
38
- # End of this component
39
- "END:#{@name.upcase}\r\n"
32
+ def properties_to_print
33
+ @properties.select { |k,v| k != 'version' }
40
34
  end
41
35
 
42
36
  def event(&block)
43
- e = Event.new
44
- # Note: I'm not sure this is the best way to pass this down, but it works
45
- e.tzid = self.timezones[0].tzid if self.timezones.length > 0
46
-
47
- self.add_component e
48
-
49
- if block
50
- e.instance_eval(&block)
51
- if e.tzid
52
- e.dtstart.ical_params = { "TZID" => e.tzid }
53
- e.dtend.ical_params = { "TZID" => e.tzid }
37
+ calendar_tzid = timezone_id
38
+ build_component Event.new do
39
+ # Note: I'm not sure this is the best way to pass this down, but it works
40
+ self.tzid = calendar_tzid
41
+
42
+ if block
43
+ instance_eval(&block)
44
+ if tzid
45
+ dtstart.ical_params = { "TZID" => e.tzid }
46
+ dtend.ical_params = { "TZID" => e.tzid } unless dtend.nil?
47
+ end
54
48
  end
55
49
  end
56
-
57
- e
58
50
  end
59
51
 
60
52
  def find_event(uid)
61
- self.events.find {|e| e.uid == uid}
53
+ events.find {|e| e.uid == uid}
62
54
  end
63
55
 
64
56
  def todo(&block)
65
- e = Todo.new
66
- self.add_component e
67
-
68
- e.instance_eval(&block) if block
69
-
70
- e
57
+ build_component Todo.new, &block
71
58
  end
72
59
 
73
60
  def find_todo(uid)
74
- self.todos.find {|t| t.uid == uid}
61
+ todos.find {|t| t.uid == uid}
75
62
  end
76
63
 
77
64
  def journal(&block)
78
- e = Journal.new
79
- self.add_component e
80
-
81
- e.instance_eval(&block) if block
82
-
83
- e
65
+ build_component Journal.new, &block
84
66
  end
85
67
 
86
68
  def find_journal(uid)
87
- self.journals.find {|j| j.uid == uid}
69
+ journals.find {|j| j.uid == uid}
88
70
  end
89
71
 
90
72
  def freebusy(&block)
91
- e = Freebusy.new
92
- self.add_component e
93
-
94
- e.instance_eval(&block) if block
95
-
96
- e
73
+ build_component Freebusy.new, &block
97
74
  end
98
75
 
99
76
  def find_freebusy(uid)
100
- self.freebusys.find {|f| f.uid == uid}
77
+ freebusys.find {|f| f.uid == uid}
101
78
  end
102
79
 
103
80
  def timezone(&block)
104
- e = Timezone.new
105
- self.add_component e
106
-
107
- e.instance_eval(&block) if block
108
-
109
- e
81
+ build_component Timezone.new, &block
110
82
  end
111
83
 
112
84
  # The "PUBLISH" method in a "VEVENT" calendar component is an
@@ -122,6 +94,17 @@ module Icalendar
122
94
  self.ip_method = "PUBLISH"
123
95
  end
124
96
 
97
+ private
98
+
99
+ def build_component(component, &block)
100
+ add_component component
101
+ component.instance_eval(&block) if block
102
+ component
103
+ end
104
+
105
+ def timezone_id
106
+ timezones[0].tzid if timezones.length > 0
107
+ end
125
108
  end # class Calendar
126
109
 
127
110
  end # module Icalendar
@@ -49,7 +49,7 @@ module Icalendar
49
49
 
50
50
  def initialize(name)
51
51
  @name = name
52
- @components = Hash.new([])
52
+ @components = Hash.new { |h, k| h[k] = [] }
53
53
  @properties = {}
54
54
 
55
55
  @@logger.info("New #{@name[1,@name.size].capitalize}...")
@@ -57,13 +57,7 @@ module Icalendar
57
57
 
58
58
  # Add a sub-component to the current component object.
59
59
  def add_component(component)
60
- key = (component.class.to_s.downcase + 's').gsub('icalendar::', '').to_sym
61
-
62
- unless @components.has_key? key
63
- @components[key] = []
64
- end
65
-
66
- @components[key] << component
60
+ @components[component.key_name] << component
67
61
  end
68
62
 
69
63
  # Add a component to the calendar.
@@ -79,11 +73,7 @@ module Icalendar
79
73
  alias add_journal add_component
80
74
 
81
75
  def remove_component(component)
82
- key = (component.class.to_s.downcase + 's').gsub('icalendar::', '').to_sym
83
-
84
- if @components.has_key? key
85
- @components[key].delete(component)
86
- end
76
+ @components[component.key_name].delete(component)
87
77
  end
88
78
 
89
79
  # Remove a component from the calendar.
@@ -105,95 +95,82 @@ module Icalendar
105
95
 
106
96
  # Output in the icalendar format
107
97
  def to_ical
108
- print_component do
109
- s = ""
110
- @components.each do |key, comps|
111
- comps.each { |component| s << component.to_ical }
112
- end
113
- s
98
+ printer do
99
+ [print_headers,
100
+ print_properties,
101
+ print_subcomponents].join
114
102
  end
115
103
  end
116
104
 
117
105
  # Print this icalendar component
118
106
  def print_component
119
- # Begin a new component
120
- "BEGIN:#{@name.upcase}\r\n" +
121
-
122
- # Then the properties
123
- print_properties +
124
-
125
- # sub components
126
- yield +
107
+ to_ical
108
+ end
127
109
 
128
- # End of this component
129
- "END:#{@name.upcase}\r\n"
110
+ def print_subcomponents
111
+ @components.values.map do |component_parts|
112
+ Array(component_parts).map &:to_ical
113
+ end.join
130
114
  end
131
115
 
132
- def print_properties(properties = @properties)
133
- s = ""
116
+ def printer
117
+ ["BEGIN:#{@name.upcase}\r\n",
118
+ yield,
119
+ "END:#{@name.upcase}\r\n"].join
120
+ end
134
121
 
135
- properties.sort.each do |key,val|
136
- # Take out underscore for property names that conflicted
137
- # with built-in words.
138
- if key =~ /ip_.*/
139
- key = key[3..-1]
122
+ def print_properties(properties = properties_to_print)
123
+ excludes = %w(geo rrule categories exdate)
124
+ properties.sort.map do |key, val|
125
+ property = fix_conflict_with_built_in(key)
126
+ prelude = property.gsub(/_/, '-').upcase
127
+ params = print_parameters(val)
128
+
129
+ value = ":#{val.to_ical}"
130
+ multiline = multiline_property?(property)
131
+ if multiline || (!multiline && !excludes.include?(property))
132
+ value = escape_chars(value)
140
133
  end
141
134
 
142
- # Property name
143
- if !multiline_property?(key)
144
- prelude = "#{key.gsub(/_/, '-').upcase}#{print_parameters val}"
145
-
146
- # Property value
147
- value = ":#{val.to_ical}"
148
- value = escape_chars(value) unless %w[geo rrule categories exdate].include?(key)
149
- add_sliced_text(s, prelude + value)
150
- else
151
- prelude = "#{key.gsub(/_/, '-').upcase}"
152
- val.each do |v|
153
- params = print_parameters(v)
154
- value = ":#{v.to_ical}"
155
- value = escape_chars(value)
156
- add_sliced_text(s, prelude + params + value)
157
- end
158
- end
159
- end
160
- s
135
+ chunk_lines(prelude + params + value)
136
+ end.join
137
+ end
138
+
139
+ # Take out underscore for property names that conflicted
140
+ # with built-in words.
141
+ def fix_conflict_with_built_in(key)
142
+ key.sub(/\Aip_/, '')
161
143
  end
162
144
 
163
145
  def escape_chars(value)
164
- v = value.gsub("\\", "\\\\").gsub("\r\n", "\n").gsub("\r", "\n").gsub("\n", "\\n").gsub(",", "\\,").gsub(";", "\\;")
165
- return v
166
- # return value
146
+ value.gsub("\\", "\\\\").gsub("\r\n", "\n").gsub("\r", "\n").gsub("\n", "\\n").gsub(",", "\\,").gsub(";", "\\;")
167
147
  end
168
148
 
169
- def add_sliced_text(add_to,escaped)
170
- escaped = escaped.split('') # split is unicdoe-aware when `$KCODE = 'u'`
171
- add_to << escaped.slice!(0,MAX_LINE_LENGTH).join << "\r\n " while escaped.length != 0 # shift(MAX_LINE_LENGTH) does not work with ruby 1.8.6
172
- add_to.gsub!(/ *$/, '')
149
+ def chunk_lines(str, length = MAX_LINE_LENGTH, separator = "\r\n ")
150
+ chunks = str.scan(/.{1,#{length}}/)
151
+ lines = chunks.join(separator) << separator
152
+ lines.gsub(/ *$/, '')
173
153
  end
174
154
 
175
155
  # Print the parameters for a specific property.
176
156
  def print_parameters(value)
177
- s = ""
178
- return s unless value.respond_to?(:ical_params) && !value.ical_params.nil?
179
-
180
- value.ical_params.each do |key, val|
181
- s << ";#{key}"
182
- val = [ val ] unless val.is_a?(Array)
183
-
184
- # Possible parameter values
185
- unless val.empty?
186
- s << "="
187
- s << val.map do |pval|
188
- if pval.respond_to? :to_ical
189
- param = pval.to_ical
190
- param = %|"#{param}"| unless param =~ %r{\A#{Parser::QSTR}\z|\A#{Parser::PTEXT}\z}
191
- param
192
- end
193
- end.compact.join(',')
194
- end
195
- end
196
- s
157
+ return "" unless value.respond_to?(:ical_params)
158
+
159
+ Array(value.ical_params).map do |key, val|
160
+ val = Array(val)
161
+ next if val.empty?
162
+
163
+ escaped = val.map { |v| Parser.escape(v.to_ical) }.join(',')
164
+ ";#{key}=" << escaped
165
+ end.join
166
+ end
167
+
168
+ def properties_to_print
169
+ @properties # subclasses can exclude properties
170
+ end
171
+
172
+ def print_headers
173
+ "" # subclasses can specify headers
197
174
  end
198
175
 
199
176
  # TODO: Look into the x-property, x-param stuff...
@@ -214,6 +191,10 @@ module Icalendar
214
191
  # Make it protected so we can monitor usage...
215
192
  protected
216
193
 
194
+ def key_name
195
+ (self.class.to_s.downcase + 's').gsub('icalendar::', '').to_sym
196
+ end
197
+
217
198
  def self.ical_component(*syms)
218
199
  hash_accessor :@components, *syms
219
200
  end
@@ -108,7 +108,7 @@ module Icalendar
108
108
 
109
109
  # Now doing some basic initialization
110
110
  sequence 0
111
- timestamp DateTime.now
111
+ timestamp Time.now.utc.to_datetime.tap { |t| t.icalendar_tzid = 'UTC' }
112
112
  uid new_uid
113
113
  end
114
114
 
@@ -31,7 +31,7 @@ module Icalendar
31
31
  def initialize()
32
32
  super("VFREEBUSY")
33
33
 
34
- timestamp DateTime.now
34
+ timestamp Time.now.utc.to_datetime.tap { |t| t.icalendar_tzid = 'UTC' }
35
35
  uid new_uid
36
36
  end
37
37
  end
@@ -52,7 +52,7 @@ module Icalendar
52
52
  super("VJOURNAL")
53
53
 
54
54
  sequence 0
55
- timestamp DateTime.now
55
+ timestamp Time.now.utc.to_datetime.tap { |t| t.icalendar_tzid = 'UTC' }
56
56
  uid new_uid
57
57
  end
58
58
 
@@ -45,19 +45,6 @@ module Icalendar
45
45
  @components[key] = component
46
46
  end
47
47
 
48
- # Also need a custom to_ical because typically it iterates over an array
49
- # of components.
50
- def to_ical
51
- print_component do
52
- s = ""
53
- @components.each_value do |comp|
54
- s << comp.to_ical
55
- end
56
- s
57
- end
58
- end
59
-
60
-
61
48
  def initialize(name = "VTIMEZONE")
62
49
  super(name)
63
50
  end
@@ -56,7 +56,7 @@ module Icalendar
56
56
  super("VTODO")
57
57
 
58
58
  sequence 0
59
- timestamp DateTime.now
59
+ timestamp Time.now.utc.to_datetime.tap { |t| t.icalendar_tzid = 'UTC' }
60
60
  uid new_uid
61
61
  end
62
62
 
@@ -65,6 +65,19 @@ module Icalendar
65
65
  @@logger.debug("New Calendar Parser: #{@file.inspect}")
66
66
  end
67
67
 
68
+ def self.escape(value)
69
+ if value =~ %r{\A#{Parser::QSTR}\z|\A#{Parser::PTEXT}\z}
70
+ value
71
+ else
72
+ stripped = value.gsub '"', "'"
73
+ if stripped =~ /\A#{Parser::PTEXT}\z/
74
+ stripped
75
+ else
76
+ %|"#{stripped}"|
77
+ end
78
+ end
79
+ end
80
+
68
81
  # Define next line for an IO object.
69
82
  # Works for strings now with StringIO
70
83
  def next_line
@@ -45,6 +45,7 @@ class TestCalendar < Test::Unit::TestCase
45
45
 
46
46
  def test_create_multiple_event_calendar
47
47
  # Create a fresh calendar
48
+ Timecop.freeze DateTime.new(2013, 12, 26, 5, 0, 0, '+0000')
48
49
  cal = Calendar.new
49
50
  [1,2,3].each do |t|
50
51
  cal.event do
@@ -57,7 +58,49 @@ class TestCalendar < Test::Unit::TestCase
57
58
  self.summary = "test #{t} todo"
58
59
  end
59
60
  end
60
- cal.to_ical
61
+ expected_no_uid = <<-EXPECTED.gsub("\n", "\r\n")
62
+ BEGIN:VCALENDAR
63
+ VERSION:2.0
64
+ CALSCALE:GREGORIAN
65
+ PRODID:iCalendar-Ruby
66
+ BEGIN:VEVENT
67
+ DTEND:19970901T190000Z
68
+ DTSTAMP:20131226T050000Z
69
+ SEQUENCE:0
70
+ SUMMARY:This is summary 1
71
+ END:VEVENT
72
+ BEGIN:VEVENT
73
+ DTEND:19970902T190000Z
74
+ DTSTAMP:20131226T050000Z
75
+ SEQUENCE:0
76
+ SUMMARY:This is summary 2
77
+ END:VEVENT
78
+ BEGIN:VEVENT
79
+ DTEND:19970903T190000Z
80
+ DTSTAMP:20131226T050000Z
81
+ SEQUENCE:0
82
+ SUMMARY:This is summary 3
83
+ END:VEVENT
84
+ BEGIN:VTODO
85
+ DTSTAMP:20131226T050000Z
86
+ SEQUENCE:0
87
+ SUMMARY:test 1 todo
88
+ END:VTODO
89
+ BEGIN:VTODO
90
+ DTSTAMP:20131226T050000Z
91
+ SEQUENCE:0
92
+ SUMMARY:test 2 todo
93
+ END:VTODO
94
+ BEGIN:VTODO
95
+ DTSTAMP:20131226T050000Z
96
+ SEQUENCE:0
97
+ SUMMARY:test 3 todo
98
+ END:VTODO
99
+ END:VCALENDAR
100
+ EXPECTED
101
+ actual_no_uid = cal.to_ical.gsub /^UID:.*\r\n(?: .*\r\n)*/, ''
102
+ Timecop.return
103
+ assert_equal expected_no_uid, actual_no_uid
61
104
  end
62
105
 
63
106
  def test_find
@@ -40,9 +40,11 @@ class TestParameter < Test::Unit::TestCase
40
40
 
41
41
  def test_unquoted_property_parameters
42
42
  params = {'ALTREP' => ['"http://my.language.net"'],
43
- 'LANGUAGE' => ['SPANISH:CATILLAN']}
43
+ 'LANGUAGE' => ['SPANISH:CATILLAN'],
44
+ 'CN' => ['Joe "The Man" Tester']}
44
45
  expected_params = {'ALTREP' => ['"http://my.language.net"'],
45
- 'LANGUAGE' => ['"SPANISH:CATILLAN"']}
46
+ 'LANGUAGE' => ['"SPANISH:CATILLAN"'],
47
+ 'CN' => ["Joe 'The Man' Tester"]}
46
48
  @event.summary('This is a test summary.', params)
47
49
 
48
50
  assert_equal params, @event.summary.ical_params
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: 1.5.0
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Ahearn
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-12-07 00:00:00.000000000 Z
11
+ date: 2014-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake