icalendar 1.5.4 → 2.0.0.beta.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 +7 -0
- data/.gitignore +1 -1
- data/.rspec +2 -0
- data/.travis.yml +1 -2
- data/History.txt +2 -7
- data/README.md +82 -107
- data/Rakefile +6 -7
- data/icalendar.gemspec +10 -9
- data/lib/icalendar.rb +17 -33
- data/lib/icalendar/alarm.rb +35 -0
- data/lib/icalendar/calendar.rb +17 -100
- data/lib/icalendar/component.rb +41 -403
- data/lib/icalendar/event.rb +51 -0
- data/lib/icalendar/freebusy.rb +27 -0
- data/lib/icalendar/has_components.rb +83 -0
- data/lib/icalendar/has_properties.rb +156 -0
- data/lib/icalendar/journal.rb +39 -0
- data/lib/icalendar/parser.rb +75 -403
- data/lib/icalendar/timezone.rb +53 -0
- data/lib/icalendar/todo.rb +52 -0
- data/lib/icalendar/tzinfo.rb +30 -30
- data/lib/icalendar/value.rb +80 -0
- data/lib/icalendar/values/array.rb +43 -0
- data/lib/icalendar/values/binary.rb +31 -0
- data/lib/icalendar/values/boolean.rb +17 -0
- data/lib/icalendar/values/cal_address.rb +8 -0
- data/lib/icalendar/values/date.rb +26 -0
- data/lib/icalendar/values/date_time.rb +34 -0
- data/lib/icalendar/values/duration.rb +48 -0
- data/lib/icalendar/values/float.rb +17 -0
- data/lib/icalendar/values/integer.rb +17 -0
- data/lib/icalendar/values/period.rb +46 -0
- data/lib/icalendar/values/recur.rb +63 -0
- data/lib/icalendar/values/text.rb +26 -0
- data/lib/icalendar/values/time.rb +34 -0
- data/lib/icalendar/values/time_with_zone.rb +31 -0
- data/lib/icalendar/values/uri.rb +19 -0
- data/lib/icalendar/values/utc_offset.rb +39 -0
- data/lib/icalendar/version.rb +5 -0
- data/spec/alarm_spec.rb +108 -0
- data/spec/calendar_spec.rb +167 -0
- data/spec/event_spec.rb +108 -0
- data/{test/fixtures/folding.ics → spec/fixtures/nondefault_values.ics} +2 -2
- data/{test → spec}/fixtures/single_event.ics +11 -14
- data/spec/fixtures/timezone.ics +35 -0
- data/spec/freebusy_spec.rb +7 -0
- data/spec/journal_spec.rb +7 -0
- data/spec/parser_spec.rb +26 -0
- data/spec/roundtrip_spec.rb +40 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/timezone_spec.rb +31 -0
- data/spec/todo_spec.rb +24 -0
- data/spec/tzinfo_spec.rb +85 -0
- data/spec/values/date_time_spec.rb +80 -0
- data/spec/values/duration_spec.rb +67 -0
- data/spec/values/period_spec.rb +47 -0
- data/spec/values/recur_spec.rb +47 -0
- data/spec/values/text_spec.rb +72 -0
- data/spec/values/utc_offset_spec.rb +41 -0
- metadata +129 -88
- data/GPL +0 -340
- data/examples/create_cal.rb +0 -45
- data/examples/parse_cal.rb +0 -20
- data/examples/single_event.ics +0 -18
- data/lib/hash_attrs.rb +0 -34
- data/lib/icalendar/base.rb +0 -47
- data/lib/icalendar/component/alarm.rb +0 -47
- data/lib/icalendar/component/event.rb +0 -131
- data/lib/icalendar/component/freebusy.rb +0 -38
- data/lib/icalendar/component/journal.rb +0 -60
- data/lib/icalendar/component/timezone.rb +0 -91
- data/lib/icalendar/component/todo.rb +0 -64
- data/lib/icalendar/conversions.rb +0 -107
- data/lib/icalendar/helpers.rb +0 -109
- data/lib/icalendar/parameter.rb +0 -33
- data/lib/icalendar/rrule.rb +0 -133
- data/lib/meta.rb +0 -32
- data/script/console +0 -10
- data/script/recur1.ics +0 -38
- data/script/tryit.rb +0 -13
- data/test/component/test_event.rb +0 -253
- data/test/component/test_timezone.rb +0 -74
- data/test/component/test_todo.rb +0 -31
- data/test/fixtures/life.ics +0 -46
- data/test/fixtures/nonstandard.ics +0 -25
- data/test/fixtures/simplecal.ics +0 -119
- data/test/interactive.rb +0 -17
- data/test/read_write.rb +0 -23
- data/test/test_calendar.rb +0 -167
- data/test/test_component.rb +0 -102
- data/test/test_conversions.rb +0 -104
- data/test/test_helper.rb +0 -7
- data/test/test_parameter.rb +0 -91
- data/test/test_parser.rb +0 -100
- data/test/test_tzinfo.rb +0 -83
- data/website/index.html +0 -70
- data/website/index.txt +0 -38
- data/website/javascripts/rounded_corners_lite.inc.js +0 -285
- data/website/stylesheets/screen.css +0 -159
- data/website/template.html.erb +0 -50
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Icalendar
|
4
|
+
module Values
|
5
|
+
|
6
|
+
class Duration < Value
|
7
|
+
|
8
|
+
def initialize(value, params = {})
|
9
|
+
super OpenStruct.new(parse_fields value), params
|
10
|
+
end
|
11
|
+
|
12
|
+
def past?
|
13
|
+
value.past
|
14
|
+
end
|
15
|
+
|
16
|
+
def value_ical
|
17
|
+
return "#{'-' if past?}P#{weeks}W" if weeks > 0
|
18
|
+
builder = []
|
19
|
+
builder << '-' if past?
|
20
|
+
builder << 'P'
|
21
|
+
builder << "#{days}D" if days > 0
|
22
|
+
builder << 'T' if time?
|
23
|
+
builder << "#{hours}H" if hours > 0
|
24
|
+
builder << "#{minutes}M" if minutes > 0
|
25
|
+
builder << "#{seconds}S" if seconds > 0
|
26
|
+
builder.join
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def time?
|
32
|
+
hours > 0 || minutes > 0 || seconds > 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def parse_fields(value)
|
36
|
+
{
|
37
|
+
past: (value =~ /\A([+-])P/ ? $1 == '-' : false),
|
38
|
+
weeks: (value =~ /(\d+)W/ ? $1.to_i : 0),
|
39
|
+
days: (value =~ /(\d+)D/ ? $1.to_i : 0),
|
40
|
+
hours: (value =~ /(\d+)H/ ? $1.to_i : 0),
|
41
|
+
minutes: (value =~ /(\d+)M/ ? $1.to_i : 0),
|
42
|
+
seconds: (value =~ /(\d+)S/ ? $1.to_i : 0)
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Icalendar
|
2
|
+
module Values
|
3
|
+
|
4
|
+
class Period < Value
|
5
|
+
|
6
|
+
def initialize(value, params = {})
|
7
|
+
parts = value.split '/'
|
8
|
+
period_start = Icalendar::Values::DateTime.new parts.first
|
9
|
+
if parts.last =~ /\A[+-]?P.+\z/
|
10
|
+
period_end = Icalendar::Values::Duration.new parts.last
|
11
|
+
else
|
12
|
+
period_end = Icalendar::Values::DateTime.new parts.last
|
13
|
+
end
|
14
|
+
super [period_start, period_end], params
|
15
|
+
end
|
16
|
+
|
17
|
+
def value_ical
|
18
|
+
value.map { |v| v.value_ical }.join '/'
|
19
|
+
end
|
20
|
+
|
21
|
+
def period_start
|
22
|
+
first
|
23
|
+
end
|
24
|
+
|
25
|
+
def period_start=(v)
|
26
|
+
value[0] = v.is_a?(Icalendar::Values::DateTime) ? v : Icalendar::Values::DateTime.new(v)
|
27
|
+
end
|
28
|
+
|
29
|
+
def explicit_end
|
30
|
+
last.is_a?(Icalendar::Values::DateTime) ? last : nil
|
31
|
+
end
|
32
|
+
|
33
|
+
def explicit_end=(v)
|
34
|
+
value[1] = v.is_a?(Icalendar::Values::DateTime) ? v : Icalendar::Values::DateTime.new(v)
|
35
|
+
end
|
36
|
+
|
37
|
+
def duration
|
38
|
+
last.is_a?(Icalendar::Values::Duration) ? last : nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def duration=(v)
|
42
|
+
value[1] = v.is_a?(Icalendar::Values::Duration) ? v : Icalendar::Values::Duration.new(v)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Icalendar
|
4
|
+
module Values
|
5
|
+
|
6
|
+
class Recur < Value
|
7
|
+
NUM_LIST = '\d{1,2}(?:,\d{1,2})*'
|
8
|
+
DAYNAME = 'SU|MO|TU|WE|TH|FR|SA'
|
9
|
+
WEEKDAY = "(?:[+-]?\\d{1,2})?(?:#{DAYNAME})"
|
10
|
+
MONTHDAY = '[+-]?\d{1,2}'
|
11
|
+
YEARDAY = '[+-]?\d{1,3}'
|
12
|
+
|
13
|
+
def initialize(value, params = {})
|
14
|
+
super OpenStruct.new(parse_fields value), params
|
15
|
+
end
|
16
|
+
|
17
|
+
def valid?
|
18
|
+
return false if frequency.nil?
|
19
|
+
return false if !self.until.nil? && !count.nil?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def value_ical
|
24
|
+
builder = ["FREQ=#{frequency}"]
|
25
|
+
builder << "UNTIL=#{self.until}" unless self.until.nil?
|
26
|
+
builder << "COUNT=#{count}" unless count.nil?
|
27
|
+
builder << "INTERVAL=#{interval}" unless interval.nil?
|
28
|
+
builder << "BYSECOND=#{by_second.join ','}" unless by_second.nil?
|
29
|
+
builder << "BYMINUTE=#{by_minute.join ','}" unless by_minute.nil?
|
30
|
+
builder << "BYHOUR=#{by_hour.join ','}" unless by_hour.nil?
|
31
|
+
builder << "BYDAY=#{by_day.join ','}" unless by_day.nil?
|
32
|
+
builder << "BYMONTHDAY=#{by_month_day.join ','}" unless by_month_day.nil?
|
33
|
+
builder << "BYYEARDAY=#{by_year_day.join ','}" unless by_year_day.nil?
|
34
|
+
builder << "BYWEEKNO=#{by_week_number.join ','}" unless by_week_number.nil?
|
35
|
+
builder << "BYMONTH=#{by_month.join ','}" unless by_month.nil?
|
36
|
+
builder << "BYSETPOS=#{by_set_position.join ','}" unless by_set_position.nil?
|
37
|
+
builder << "WKST=#{week_start}" unless week_start.nil?
|
38
|
+
builder.join ';'
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def parse_fields(value)
|
44
|
+
{
|
45
|
+
frequency: (value =~ /FREQ=(SECONDLY|MINUTELY|HOURLY|DAILY|WEEKLY|MONTHLY|YEARLY)/i ? $1.upcase : nil),
|
46
|
+
until: (value =~ /UNTIL=([^;]*)/i ? $1 : nil),
|
47
|
+
count: (value =~ /COUNT=(\d+)/i ? $1.to_i : nil),
|
48
|
+
interval: (value =~ /INTERVAL=(\d+)/i ? $1.to_i : nil),
|
49
|
+
by_second: (value =~ /BYSECOND=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
|
50
|
+
by_minute: (value =~ /BYMINUTE=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
|
51
|
+
by_hour: (value =~ /BYHOUR=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
|
52
|
+
by_day: (value =~ /BYDAY=(#{WEEKDAY}(?:,#{WEEKDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
|
53
|
+
by_month_day: (value =~ /BYMONTHDAY=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
|
54
|
+
by_year_day: (value =~ /BYYEARDAY=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
|
55
|
+
by_week_number: (value =~ /BYWEEKNO=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
|
56
|
+
by_month: (value =~ /BYMONTH=(#{NUM_LIST})(?:;|\z)/i ? $1.split(',').map { |i| i.to_i } : nil),
|
57
|
+
by_set_position: (value =~ /BYSETPOS=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i ? $1.split(',') : nil),
|
58
|
+
week_start: (value =~ /WKST=(#{DAYNAME})/i ? $1.upcase : nil)
|
59
|
+
}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Icalendar
|
2
|
+
module Values
|
3
|
+
|
4
|
+
class Text < Value
|
5
|
+
|
6
|
+
def initialize(value, params = {})
|
7
|
+
value = value.gsub '\n', "\n"
|
8
|
+
value.gsub! '\,', ','
|
9
|
+
value.gsub! '\;', ';'
|
10
|
+
value.gsub!('\\\\') { '\\' }
|
11
|
+
super value, params
|
12
|
+
end
|
13
|
+
|
14
|
+
def value_ical
|
15
|
+
value.dup.tap do |v|
|
16
|
+
v.gsub!('\\') { '\\\\' }
|
17
|
+
v.gsub! ';', '\;'
|
18
|
+
v.gsub! ',', '\,'
|
19
|
+
v.gsub! /\r?\n/, '\n'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'date'
|
2
|
+
require_relative 'time_with_zone'
|
3
|
+
|
4
|
+
module Icalendar
|
5
|
+
module Values
|
6
|
+
|
7
|
+
class Time < Value
|
8
|
+
include TimeWithZone
|
9
|
+
|
10
|
+
FORMAT = '%H%M%S'
|
11
|
+
|
12
|
+
def initialize(value, params = {})
|
13
|
+
if value.is_a? String
|
14
|
+
params['tzid'] = 'UTC' if value.end_with? 'Z'
|
15
|
+
super ::DateTime.strptime(value, FORMAT).to_time, params
|
16
|
+
elsif value.respond_to? :to_time
|
17
|
+
super value.to_time, params
|
18
|
+
else
|
19
|
+
super
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def value_ical
|
24
|
+
if tz_utc
|
25
|
+
"#{strftime FORMAT}Z"
|
26
|
+
else
|
27
|
+
strftime FORMAT
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
begin
|
2
|
+
require 'active_support/time'
|
3
|
+
rescue LoadError
|
4
|
+
$stderr.puts "Info: No TimeWithZone support"
|
5
|
+
end
|
6
|
+
|
7
|
+
module Icalendar
|
8
|
+
module Values
|
9
|
+
module TimeWithZone
|
10
|
+
attr_reader :tz_utc
|
11
|
+
|
12
|
+
def initialize(value, params = {})
|
13
|
+
@tz_utc = params['tzid'] == 'UTC'
|
14
|
+
|
15
|
+
if defined?(ActiveSupport) && !params['tzid'].nil?
|
16
|
+
tzid = params['tzid'].is_a?(::Array) ? params['tzid'].first : params['tzid']
|
17
|
+
zone = ActiveSupport::TimeZone[tzid]
|
18
|
+
value = ActiveSupport::TimeWithZone.new nil, zone, value unless zone.nil?
|
19
|
+
super value, params
|
20
|
+
else
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def params_ical
|
26
|
+
ical_params.delete 'tzid' if tz_utc
|
27
|
+
super
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Icalendar
|
4
|
+
module Values
|
5
|
+
|
6
|
+
class Uri < Value
|
7
|
+
|
8
|
+
def initialize(value, params = {})
|
9
|
+
parsed = URI.parse value rescue value
|
10
|
+
super parsed, params
|
11
|
+
end
|
12
|
+
|
13
|
+
def value_ical
|
14
|
+
value.to_s
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'ostruct'
|
2
|
+
|
3
|
+
module Icalendar
|
4
|
+
module Values
|
5
|
+
|
6
|
+
class UtcOffset < Value
|
7
|
+
|
8
|
+
def initialize(value, params = {})
|
9
|
+
value = OpenStruct.new parse_fields(value)
|
10
|
+
super value, params
|
11
|
+
end
|
12
|
+
|
13
|
+
def behind?
|
14
|
+
return false if zero_offset?
|
15
|
+
value.behind
|
16
|
+
end
|
17
|
+
|
18
|
+
def value_ical
|
19
|
+
"#{behind? ? '-' : '+'}#{'%02d' % hours}#{'%02d' % minutes}#{'%02d' % seconds if seconds > 0}"
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def zero_offset?
|
25
|
+
hours == 0 && minutes == 0 && seconds == 0
|
26
|
+
end
|
27
|
+
|
28
|
+
def parse_fields(value)
|
29
|
+
md = /\A(?<behind>[+-])(?<hours>\d{2})(?<minutes>\d{2})(?<seconds>\d{2})?\z/.match value
|
30
|
+
{
|
31
|
+
behind: (md[:behind] == '-'),
|
32
|
+
hours: md[:hours].to_i,
|
33
|
+
minutes: md[:minutes].to_i,
|
34
|
+
seconds: md[:seconds].to_i
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
data/spec/alarm_spec.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Icalendar::Alarm do
|
4
|
+
|
5
|
+
# currently no behavior in Alarm not tested other places
|
6
|
+
describe '#valid?' do
|
7
|
+
subject do
|
8
|
+
described_class.new.tap do |a|
|
9
|
+
a.action = 'AUDIO'
|
10
|
+
a.trigger = Time.now.utc.to_datetime
|
11
|
+
end
|
12
|
+
end
|
13
|
+
context 'neither duration or repeat is set' do
|
14
|
+
it { should be_valid }
|
15
|
+
end
|
16
|
+
context 'both duration and repeat are set' do
|
17
|
+
before(:each) do
|
18
|
+
subject.duration = 'PT15M'
|
19
|
+
subject.repeat = 4
|
20
|
+
end
|
21
|
+
it { should be_valid }
|
22
|
+
end
|
23
|
+
context 'only duration is set' do
|
24
|
+
before(:each) { subject.duration = 'PT15M' }
|
25
|
+
it { should_not be_valid }
|
26
|
+
end
|
27
|
+
context 'only repeat is set' do
|
28
|
+
before(:each) { subject.repeat = 4 }
|
29
|
+
it { should_not be_valid }
|
30
|
+
end
|
31
|
+
|
32
|
+
context 'display action' do
|
33
|
+
before(:each) { subject.action = 'DISPLAY' }
|
34
|
+
it 'requires description' do
|
35
|
+
subject.should_not be_valid
|
36
|
+
subject.description = 'Display Text'
|
37
|
+
subject.should be_valid
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'email action' do
|
42
|
+
before(:each) { subject.action = 'EMAIL' }
|
43
|
+
context 'requires subject and body' do
|
44
|
+
before(:each) { subject.attendee = ['mailto:test@email.com'] }
|
45
|
+
it 'requires description' do
|
46
|
+
subject.summary = 'Email subject'
|
47
|
+
subject.should_not be_valid
|
48
|
+
subject.description = 'Email Body'
|
49
|
+
subject.should be_valid
|
50
|
+
end
|
51
|
+
it 'requires summary' do
|
52
|
+
subject.description = 'Email body'
|
53
|
+
subject.should_not be_valid
|
54
|
+
subject.summary = 'Email subject'
|
55
|
+
subject.should be_valid
|
56
|
+
end
|
57
|
+
end
|
58
|
+
context 'attendees are required' do
|
59
|
+
before(:each) do
|
60
|
+
subject.summary = 'subject'
|
61
|
+
subject.description = 'body'
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'must be present' do
|
65
|
+
subject.attendee = nil
|
66
|
+
subject.should_not be_valid
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'can be single' do
|
70
|
+
subject.attendee << 'mailto:test@email.com'
|
71
|
+
subject.should be_valid
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'can be multi' do
|
75
|
+
subject.attendee << 'mailto:test@email.com'
|
76
|
+
subject.attendee << 'mailto:email@test.com'
|
77
|
+
subject.should be_valid
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context 'strict validations check parent' do
|
83
|
+
subject do
|
84
|
+
described_class.new.tap do |a|
|
85
|
+
a.action = 'AUDIO'
|
86
|
+
a.trigger = Time.now.utc.to_datetime
|
87
|
+
end
|
88
|
+
end
|
89
|
+
specify { subject.valid?(true).should be_true }
|
90
|
+
context 'with parent' do
|
91
|
+
before(:each) { subject.parent = parent }
|
92
|
+
context 'event' do
|
93
|
+
let(:parent) { Icalendar::Event.new }
|
94
|
+
specify { subject.valid?(true).should be_true }
|
95
|
+
end
|
96
|
+
context 'todo' do
|
97
|
+
let(:parent) { Icalendar::Todo.new }
|
98
|
+
specify { subject.valid?(true).should be_true }
|
99
|
+
end
|
100
|
+
context 'journal' do
|
101
|
+
let(:parent) { Icalendar::Journal.new }
|
102
|
+
specify { subject.valid?(true).should be_false }
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|