icalendar-2.10.3/0000755000004100000410000000000014715202173013551 5ustar www-datawww-dataicalendar-2.10.3/.gitignore0000644000004100000410000000012114715202173015533 0ustar www-datawww-data.rvmrc .ruby-version .ruby-gemset Gemfile.lock pkg/ .bundle coverage/ tags .idea icalendar-2.10.3/.github/0000755000004100000410000000000014715202173015111 5ustar www-datawww-dataicalendar-2.10.3/.github/workflows/0000755000004100000410000000000014715202173017146 5ustar www-datawww-dataicalendar-2.10.3/.github/workflows/main.yml0000644000004100000410000000101214715202173020607 0ustar www-datawww-dataname: Ruby on: push: branches: - main - master pull_request: branches: - main - master jobs: build: runs-on: ubuntu-latest strategy: fail-fast: false matrix: ruby: - 3.1 - 3.2 - 3.3 steps: - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} bundler-cache: true - name: Run rspec tests run: bundle exec rake spec icalendar-2.10.3/lib/0000755000004100000410000000000014715202173014317 5ustar www-datawww-dataicalendar-2.10.3/lib/icalendar/0000755000004100000410000000000014715202173016241 5ustar www-datawww-dataicalendar-2.10.3/lib/icalendar/alarm.rb0000644000004100000410000000305314715202173017663 0ustar www-datawww-data# frozen_string_literal: true module Icalendar class Alarm < Component required_property :action required_property :trigger, Icalendar::Values::Duration required_property :description, Icalendar::Values::Text, ->(alarm, description) { alarm.action.downcase == 'audio' || !description.nil? } required_property :summary, Icalendar::Values::Text, ->(alarm, summary) { alarm.action.downcase != 'email' || !summary.nil? } required_multi_property :attendee, Icalendar::Values::CalAddress, ->(alarm, attendees) { alarm.action.downcase != 'email' || !attendees.compact.empty? } optional_single_property :duration, Icalendar::Values::Duration optional_single_property :repeat, Icalendar::Values::Integer optional_property :attach, Icalendar::Values::Uri # not part of base spec - need better abstraction for extensions optional_single_property :uid optional_single_property :acknowledged, Icalendar::Values::DateTime def initialize super 'alarm' self.action = 'DISPLAY' end def valid?(strict = false) if strict # must be part of event or todo !(parent.nil? || parent.name == 'event' || parent.name == 'todo') and return false end # either both duration and repeat or neither should be set [duration, repeat].compact.size == 1 and return false # attach must be single for audio actions action.downcase == 'audio' && attach.compact.size > 1 and return false super end end end icalendar-2.10.3/lib/icalendar/logger.rb0000644000004100000410000000041714715202173020047 0ustar www-datawww-data# frozen_string_literal: true require 'delegate' require 'logger' module Icalendar class Logger < ::SimpleDelegator def initialize(sink, level = ::Logger::WARN) logger = ::Logger.new(sink) logger.level = level super logger end end end icalendar-2.10.3/lib/icalendar/todo.rb0000644000004100000410000000435214715202173017537 0ustar www-datawww-data# frozen_string_literal: true module Icalendar class Todo < Component required_property :dtstamp, Icalendar::Values::DateTime required_property :uid # dtstart only required if duration is specified required_property :dtstart, Icalendar::Values::DateTime, ->(todo, dtstart) { !(!todo.duration.nil? && dtstart.nil?) } optional_single_property :due, Icalendar::Values::DateTime optional_single_property :duration, Icalendar::Values::Duration mutually_exclusive_properties :due, :duration optional_single_property :ip_class optional_single_property :color optional_single_property :completed, Icalendar::Values::DateTime optional_single_property :created, Icalendar::Values::DateTime optional_single_property :description optional_single_property :geo, Icalendar::Values::Float optional_single_property :last_modified, Icalendar::Values::DateTime optional_single_property :location optional_single_property :organizer, Icalendar::Values::CalAddress optional_single_property :percent_complete, Icalendar::Values::Integer optional_single_property :priority, Icalendar::Values::Integer optional_single_property :recurrence_id, Icalendar::Values::DateTime optional_single_property :sequence, Icalendar::Values::Integer optional_single_property :status optional_single_property :summary optional_single_property :url, Icalendar::Values::Uri optional_property :rrule, Icalendar::Values::Recur, true optional_property :attach, Icalendar::Values::Uri optional_property :attendee, Icalendar::Values::CalAddress optional_property :categories optional_property :comment optional_property :contact optional_property :exdate, Icalendar::Values::DateTime optional_property :request_status optional_property :related_to optional_property :resources optional_property :rdate, Icalendar::Values::DateTime optional_property :conference, Icalendar::Values::Uri, false, true optional_property :image, Icalendar::Values::Uri, false, true component :alarm, false def initialize super 'todo' self.dtstamp = Icalendar::Values::DateTime.new Time.now.utc, 'tzid' => 'UTC' self.uid = new_uid end end end icalendar-2.10.3/lib/icalendar/freebusy.rb0000644000004100000410000000147414715202173020420 0ustar www-datawww-data# frozen_string_literal: true module Icalendar class Freebusy < Component required_property :dtstamp, Icalendar::Values::DateTime required_property :uid optional_single_property :contact optional_single_property :dtstart, Icalendar::Values::DateTime optional_single_property :dtend, Icalendar::Values::DateTime optional_single_property :organizer, Icalendar::Values::CalAddress optional_single_property :url, Icalendar::Values::Uri optional_property :attendee, Icalendar::Values::CalAddress optional_property :comment optional_property :freebusy, Icalendar::Values::Period optional_property :request_status def initialize super 'freebusy' self.dtstamp = Icalendar::Values::DateTime.new Time.now.utc, 'tzid' => 'UTC' self.uid = new_uid end end endicalendar-2.10.3/lib/icalendar/downcased_hash.rb0000644000004100000410000000145214715202173021542 0ustar www-datawww-data# frozen_string_literal: true require 'delegate' module Icalendar class DowncasedHash < ::SimpleDelegator def initialize(base) super Hash.new base.each do |key, value| self[key] = value end end def []=(key, value) __getobj__[key.to_s.downcase] = value end def [](key) __getobj__[key.to_s.downcase] end def has_key?(key) __getobj__.has_key? key.to_s.downcase end alias_method :include?, :has_key? alias_method :member?, :has_key? def delete(key, &block) __getobj__.delete key.to_s.downcase, &block end end def self.DowncasedHash(base) case base when Icalendar::DowncasedHash then base when Hash then Icalendar::DowncasedHash.new(base) else fail ArgumentError end end end icalendar-2.10.3/lib/icalendar/tzinfo.rb0000644000004100000410000001203014715202173020073 0ustar www-datawww-data# frozen_string_literal: true =begin Copyright (C) 2008 Sean Dague This library is free software; you can redistribute it and/or modify it under the same terms as the ruby language itself, see the file COPYING for details. =end # The following adds a bunch of mixins to the tzinfo class, with the # intent on making it very easy to load in tzinfo data for generating # ical events. With this you can do the following: # # require "icalendar/tzinfo" # # estart = DateTime.new(2008, 12, 29, 8, 0, 0) # eend = DateTime.new(2008, 12, 29, 11, 0, 0) # tstring = "America/Chicago" # # tz = TZInfo::Timezone.get(tstring) # cal = Calendar.new # # the mixins now generate all the timezone info for the date in question # timezone = tz.ical_timezone(estart) # cal.add(timezone) # # cal.event do # dtstart estart # dtend eend # summary "Meeting with the man." # description "Have a long lunch meeting and decide nothing..." # klass "PRIVATE" # end # # puts cal.to_ical # # The recurance rule calculations are hacky, and only start at the # beginning of the current dst transition. I doubt this works for non # dst areas yet. However, for a standard dst flipping zone, this # seems to work fine (tested in Mozilla Thunderbird + Lightning). # Future goal would be making this better. require 'tzinfo' begin require 'tzinfo/data' rescue LoadError Icalendar.logger.info "Could not load tzinfo/data, hopefully tzinfo is accurate (ignore for tzinfo 0.x)" end module Icalendar module TimezoneTransition def offset_from previous_offset.ical_offset end def offset_to offset.ical_offset end def offset_abbreviation offset.abbreviation.to_s end def rrule start = (respond_to?(:local_start_at) ? local_start_at : local_start).to_datetime # this is somewhat of a hack, but seems to work ok # assumes that no timezone transition is in law as "4th X of the month" # but only as 1st X, 2nd X, 3rd X, or Last X start_week = ((start.day - 1) / 7).to_i + 1 start_week = (start_week > 3) ? -1 : start_week [sprintf( 'FREQ=YEARLY;BYMONTH=%d;BYDAY=%d%s', start.month, start_week, start.strftime('%a').upcase[0,2] )] end def dtstart (respond_to?(:local_start_at) ? local_start_at : local_start).to_datetime.strftime '%Y%m%dT%H%M%S' end end module TimezoneOffset def ical_offset o = utc_total_offset sprintf '%+-2.2d%2.2d', (o / 3600).to_i, ((o / 60) % 60).to_i end end end module TZInfo class Timezone def ical_timezone(date, dst = Timezone.default_dst) period = period_for_local(date, dst) timezone = Icalendar::Timezone.new timezone.tzid = identifier if period.start_transition.nil? timezone.add_component period.single elsif period.end_transition.nil? timezone.add_component period.dst? ? period.daylight : period.standard else timezone.add_component period.daylight timezone.add_component period.standard end timezone end end if defined? TimezoneTransitionInfo class TimezoneTransitionInfo include Icalendar::TimezoneTransition end else class TimezoneTransition include Icalendar::TimezoneTransition end end if defined? TimezoneOffsetInfo class TimezoneOffsetInfo include Icalendar::TimezoneOffset end else class TimezoneOffset include Icalendar::TimezoneOffset end end class TimezonePeriod # For DST, use the start_transition, # for standard TZ, use the following period (starting from the end_transition). def daylight transition = dst? ? start_transition : end_transition day = Icalendar::Timezone::Daylight.new build_timezone(day, transition) do |tz| # rrule should not be set for the current [==DST/daylight] period # if there is no recurrence rule for the end transition if !dst? || !end_transition.nil? tz.rrule = transition.rrule end end end # For standard TZ, use the start_transition, # for DST, use the following period, (starting from the end_transition) def standard transition = dst? ? end_transition : start_transition std = Icalendar::Timezone::Standard.new build_timezone(std, transition) do |tz| if dst? || !end_transition.nil? tz.rrule = transition.rrule end end end def single Icalendar::Timezone::Standard.new.tap do |std| std.tzname = abbreviation.to_s std.tzoffsetfrom = offset.ical_offset std.tzoffsetto = offset.ical_offset std.dtstart = DateTime.new(1970).strftime '%Y%m%dT%H%M%S' end end private def build_timezone(timezone, transition) timezone.tap do |tz| tz.tzname = transition.offset_abbreviation tz.tzoffsetfrom = transition.offset_from tz.tzoffsetto = transition.offset_to tz.dtstart = transition.dtstart yield tz end end end end icalendar-2.10.3/lib/icalendar/values/0000755000004100000410000000000014715202173017540 5ustar www-datawww-dataicalendar-2.10.3/lib/icalendar/values/float.rb0000644000004100000410000000036414715202173021175 0ustar www-datawww-data# frozen_string_literal: true module Icalendar module Values class Float < Value def initialize(value, params = {}) super value.to_f, params end def value_ical value.to_s end end end endicalendar-2.10.3/lib/icalendar/values/date.rb0000644000004100000410000000171414715202173021005 0ustar www-datawww-data# frozen_string_literal: true require 'date' module Icalendar module Values class Date < Value FORMAT = '%Y%m%d' def initialize(value, params = {}) params.delete 'tzid' params.delete 'x-tz-store' if value.is_a? String begin parsed_date = ::Date.strptime(value, FORMAT) rescue ArgumentError => e raise FormatError.new("Failed to parse \"#{value}\" - #{e.message}") end super parsed_date, params elsif value.respond_to? :to_date super value.to_date, params else super end end def value_ical value.strftime FORMAT end def <=>(other) if other.is_a?(Icalendar::Values::Date) || other.is_a?(Icalendar::Values::DateTime) value_ical <=> other.value_ical else nil end end class FormatError < ArgumentError end end end end icalendar-2.10.3/lib/icalendar/values/duration.rb0000644000004100000410000000312214715202173021710 0ustar www-datawww-data# frozen_string_literal: true require 'ostruct' module Icalendar module Values class Duration < Value def initialize(value, params = {}) if value.is_a? Icalendar::Values::Duration super value.value, params else super OpenStruct.new(parse_fields value), params end end def past? value.past end def value_ical return "#{'-' if past?}P#{weeks}W" if weeks > 0 builder = [] builder << '-' if past? builder << 'P' builder << "#{days}D" if days > 0 builder << 'T' if time? builder << "#{hours}H" if hours > 0 builder << "#{minutes}M" if minutes > 0 builder << "#{seconds}S" if seconds > 0 builder.join end private def time? hours > 0 || minutes > 0 || seconds > 0 end DURATION_PAST_REGEX = /\A([+-])P/.freeze DURATION_WEEKS_REGEX = /(\d+)W/.freeze DURATION_DAYS_REGEX = /(\d+)D/.freeze DURATION_HOURS_REGEX = /(\d+)H/.freeze DURATION_MINUTES_REGEX = /(\d+)M/.freeze DURATION_SECONDS_REGEX = /(\d+)S/.freeze def parse_fields(value) { past: (value =~ DURATION_PAST_REGEX ? $1 == '-' : false), weeks: (value =~ DURATION_WEEKS_REGEX ? $1.to_i : 0), days: (value =~ DURATION_DAYS_REGEX ? $1.to_i : 0), hours: (value =~ DURATION_HOURS_REGEX ? $1.to_i : 0), minutes: (value =~ DURATION_MINUTES_REGEX ? $1.to_i : 0), seconds: (value =~ DURATION_SECONDS_REGEX ? $1.to_i : 0) } end end end end icalendar-2.10.3/lib/icalendar/values/integer.rb0000644000004100000410000000036614715202173021527 0ustar www-datawww-data# frozen_string_literal: true module Icalendar module Values class Integer < Value def initialize(value, params = {}) super value.to_i, params end def value_ical value.to_s end end end endicalendar-2.10.3/lib/icalendar/values/text.rb0000644000004100000410000000114514715202173021052 0ustar www-datawww-data# frozen_string_literal: true module Icalendar module Values class Text < Value def initialize(value, params = {}) value = value.gsub('\n', "\n") value.gsub!('\,', ',') value.gsub!('\;', ';') value.gsub!('\\\\') { '\\' } super value, params end VALUE_ICAL_CARRIAGE_RETURN_GSUB_REGEX = /\r?\n/.freeze def value_ical value.dup.tap do |v| v.gsub!('\\') { '\\\\' } v.gsub!(';', '\;') v.gsub!(',', '\,') v.gsub!(VALUE_ICAL_CARRIAGE_RETURN_GSUB_REGEX, '\n') end end end end end icalendar-2.10.3/lib/icalendar/values/recur.rb0000644000004100000410000001005214715202173021203 0ustar www-datawww-data# frozen_string_literal: true require 'ostruct' module Icalendar module Values class Recur < Value NUM_LIST = '\d{1,2}(?:,\d{1,2})*' DAYNAME = 'SU|MO|TU|WE|TH|FR|SA' WEEKDAY = "(?:[+-]?\\d{1,2})?(?:#{DAYNAME})" MONTHDAY = '[+-]?\d{1,2}' YEARDAY = '[+-]?\d{1,3}' def initialize(value, params = {}) if value.is_a? Icalendar::Values::Recur super value.value, params else super OpenStruct.new(parse_fields value), params end end def valid? return false if frequency.nil? return false if !self.until.nil? && !count.nil? true end def value_ical builder = ["FREQ=#{frequency}"] builder << "UNTIL=#{self.until}" unless self.until.nil? builder << "COUNT=#{count}" unless count.nil? builder << "INTERVAL=#{interval}" unless interval.nil? builder << "BYSECOND=#{by_second.join ','}" unless by_second.nil? builder << "BYMINUTE=#{by_minute.join ','}" unless by_minute.nil? builder << "BYHOUR=#{by_hour.join ','}" unless by_hour.nil? builder << "BYDAY=#{by_day.join ','}" unless by_day.nil? builder << "BYMONTHDAY=#{by_month_day.join ','}" unless by_month_day.nil? builder << "BYYEARDAY=#{by_year_day.join ','}" unless by_year_day.nil? builder << "BYWEEKNO=#{by_week_number.join ','}" unless by_week_number.nil? builder << "BYMONTH=#{by_month.join ','}" unless by_month.nil? builder << "BYSETPOS=#{by_set_position.join ','}" unless by_set_position.nil? builder << "WKST=#{week_start}" unless week_start.nil? builder.join ';' end private PARSE_FIELDS_FREQUENCY_REGEX = /FREQ=(SECONDLY|MINUTELY|HOURLY|DAILY|WEEKLY|MONTHLY|YEARLY)/i.freeze PARSE_FIELDS_UNTIL_REGEX = /UNTIL=([^;]*)/i.freeze PARSE_FIELDS_COUNT_REGEX = /COUNT=(\d+)/i.freeze PARSE_FIELDS_INTERVAL_REGEX = /INTERVAL=(\d+)/i.freeze PARSE_FIELDS_BY_SECOND_REGEX = /BYSECOND=(#{NUM_LIST})(?:;|\z)/i.freeze PARSE_FIELDS_BY_MINUTE_REGEX = /BYMINUTE=(#{NUM_LIST})(?:;|\z)/i.freeze PARSE_FIELDS_BY_HOUR_REGEX = /BYHOUR=(#{NUM_LIST})(?:;|\z)/i.freeze PARSE_FIELDS_BY_DAY_REGEX = /BYDAY=(#{WEEKDAY}(?:,#{WEEKDAY})*)(?:;|\z)/i.freeze PARSE_FIELDS_BY_MONTH_DAY_REGEX = /BYMONTHDAY=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i.freeze PARSE_FIELDS_BY_YEAR_DAY_REGEX = /BYYEARDAY=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i.freeze PARSE_FIELDS_BY_WEEK_NUMBER_REGEX = /BYWEEKNO=(#{MONTHDAY}(?:,#{MONTHDAY})*)(?:;|\z)/i.freeze PARSE_FIELDS_BY_MONTH_REGEX = /BYMONTH=(#{NUM_LIST})(?:;|\z)/i.freeze PARSE_FIELDS_BY_SET_POSITON_REGEX = /BYSETPOS=(#{YEARDAY}(?:,#{YEARDAY})*)(?:;|\z)/i.freeze PARSE_FIELDS_BY_WEEK_START_REGEX = /WKST=(#{DAYNAME})/i.freeze def parse_fields(value) { frequency: (value =~ PARSE_FIELDS_FREQUENCY_REGEX ? $1.upcase : nil), until: (value =~ PARSE_FIELDS_UNTIL_REGEX ? $1 : nil), count: (value =~ PARSE_FIELDS_COUNT_REGEX ? $1.to_i : nil), interval: (value =~ PARSE_FIELDS_INTERVAL_REGEX ? $1.to_i : nil), by_second: (value =~ PARSE_FIELDS_BY_SECOND_REGEX ? $1.split(',').map { |i| i.to_i } : nil), by_minute: (value =~ PARSE_FIELDS_BY_MINUTE_REGEX ? $1.split(',').map { |i| i.to_i } : nil), by_hour: (value =~ PARSE_FIELDS_BY_HOUR_REGEX ? $1.split(',').map { |i| i.to_i } : nil), by_day: (value =~ PARSE_FIELDS_BY_DAY_REGEX ? $1.split(',') : nil), by_month_day: (value =~ PARSE_FIELDS_BY_MONTH_DAY_REGEX ? $1.split(',') : nil), by_year_day: (value =~ PARSE_FIELDS_BY_YEAR_DAY_REGEX ? $1.split(',') : nil), by_week_number: (value =~ PARSE_FIELDS_BY_WEEK_NUMBER_REGEX ? $1.split(',') : nil), by_month: (value =~ PARSE_FIELDS_BY_MONTH_REGEX ? $1.split(',').map { |i| i.to_i } : nil), by_set_position: (value =~ PARSE_FIELDS_BY_SET_POSITON_REGEX ? $1.split(',') : nil), week_start: (value =~ PARSE_FIELDS_BY_WEEK_START_REGEX ? $1.upcase : nil) } end end end end icalendar-2.10.3/lib/icalendar/values/period.rb0000644000004100000410000000240114715202173021344 0ustar www-datawww-data# frozen_string_literal: true module Icalendar module Values class Period < Value PERIOD_LAST_PART_REGEX = /\A[+-]?P.+\z/.freeze def initialize(value, params = {}) parts = value.split '/' period_start = Icalendar::Values::DateTime.new parts.first if parts.last =~ PERIOD_LAST_PART_REGEX period_end = Icalendar::Values::Duration.new parts.last else period_end = Icalendar::Values::DateTime.new parts.last end super [period_start, period_end], params end def value_ical value.map { |v| v.value_ical }.join '/' end def period_start first end def period_start=(v) value[0] = v.is_a?(Icalendar::Values::DateTime) ? v : Icalendar::Values::DateTime.new(v) end def explicit_end last.is_a?(Icalendar::Values::DateTime) ? last : nil end def explicit_end=(v) value[1] = v.is_a?(Icalendar::Values::DateTime) ? v : Icalendar::Values::DateTime.new(v) end def duration last.is_a?(Icalendar::Values::Duration) ? last : nil end def duration=(v) value[1] = v.is_a?(Icalendar::Values::Duration) ? v : Icalendar::Values::Duration.new(v) end end end endicalendar-2.10.3/lib/icalendar/values/utc_offset.rb0000644000004100000410000000252114715202173022226 0ustar www-datawww-data# frozen_string_literal: true require 'ostruct' module Icalendar module Values class UtcOffset < Value def initialize(value, params = {}) if value.is_a? Icalendar::Values::UtcOffset value = value.value else value = OpenStruct.new parse_fields(value) end super value, params end def behind? return false if zero_offset? value.behind end def value_ical "#{behind? ? '-' : '+'}#{'%02d' % hours}#{'%02d' % minutes}#{'%02d' % seconds if seconds > 0}" end def to_s str = "#{behind? ? '-' : '+'}#{'%02d' % hours}:#{'%02d' % minutes}" if seconds > 0 "#{str}:#{'%02d' % seconds}" else str end end private def zero_offset? hours == 0 && minutes == 0 && seconds == 0 end PARSE_FIELDS_MD_REGEX = /\A(?[+-])(?\d{2})(?\d{2})(?\d{2})?\z/.freeze PARSE_FIELDS_WHITESPACE_GSUB_REGEX = /\s+/.freeze def parse_fields(value) md = PARSE_FIELDS_MD_REGEX.match value.gsub(PARSE_FIELDS_WHITESPACE_GSUB_REGEX, '') { behind: (md[:behind] == '-'), hours: md[:hours].to_i, minutes: md[:minutes].to_i, seconds: md[:seconds].to_i } end end end end icalendar-2.10.3/lib/icalendar/values/helpers/0000755000004100000410000000000014715202173021202 5ustar www-datawww-dataicalendar-2.10.3/lib/icalendar/values/helpers/time_with_zone.rb0000644000004100000410000000557714715202173024571 0ustar www-datawww-data# frozen_string_literal: true begin require 'active_support' require 'active_support/time' if defined?(ActiveSupport::TimeWithZone) require_relative 'active_support_time_with_zone_adapter' end rescue LoadError # tis ok, just a bit less fancy end module Icalendar module Values module Helpers module TimeWithZone attr_reader :tz_utc, :timezone_store def initialize(value, params = {}) params = Icalendar::DowncasedHash(params) @tz_utc = params['tzid'] == 'UTC' @timezone_store = params.delete 'x-tz-store' super (offset_value(value, params) || value), params end def __getobj__ orig_value = super if set_offset? orig_value else offset = offset_value(orig_value, ical_params) __setobj__(offset) unless offset.nil? offset || orig_value end end def params_ical ical_params.delete 'tzid' if tz_utc super end private def offset_value(value, params) @offset_value = unless params.nil? || params['tzid'].nil? tzid = params['tzid'].is_a?(::Array) ? params['tzid'].first : params['tzid'] support_classes_defined = defined?(ActiveSupport::TimeZone) && defined?(ActiveSupportTimeWithZoneAdapter) if support_classes_defined && (tz = ActiveSupport::TimeZone[tzid]) Icalendar.logger.debug("Plan a - parsing #{value}/#{tzid} as ActiveSupport::TimeWithZone") # plan a - use ActiveSupport::TimeWithZone ActiveSupportTimeWithZoneAdapter.new(nil, tz, value) elsif !timezone_store.nil? && !(x_tz_info = timezone_store.retrieve(tzid)).nil? # plan b - use definition from provided `VTIMEZONE` offset = x_tz_info.offset_for_local(value).to_s Icalendar.logger.debug("Plan b - parsing #{value} with offset: #{offset}") if value.respond_to?(:change) value.change offset: offset else ::Time.new value.year, value.month, value.day, value.hour, value.min, value.sec, offset end elsif support_classes_defined && (tz = ActiveSupport::TimeZone[tzid.split.first]) # plan c - try to find an ActiveSupport::TimeWithZone based on the first word of the tzid Icalendar.logger.debug("Plan c - parsing #{value}/#{tz.tzinfo.name} as ActiveSupport::TimeWithZone") params['tzid'] = [tz.tzinfo.name] ActiveSupportTimeWithZoneAdapter.new(nil, tz, value) else # plan d - just ignore the tzid Icalendar.logger.info("Ignoring timezone #{tzid} for time #{value}") nil end end end def set_offset? !!@offset_value end end end end end icalendar-2.10.3/lib/icalendar/values/helpers/active_support_time_with_zone_adapter.rb0000644000004100000410000000111514715202173031400 0ustar www-datawww-data# frozen_string_literal: true module Icalendar module Values module Helpers class ActiveSupportTimeWithZoneAdapter < ActiveSupport::TimeWithZone # ActiveSupport::TimeWithZone implements a #to_a method that will cause # unexpected behavior in components with multi_property DateTime # properties when the setters for those properties are invoked with an # Icalendar::Values::DateTime that is delegating for an # ActiveSupport::TimeWithZone. To avoid this behavior, undefine #to_a. undef_method :to_a end end end end icalendar-2.10.3/lib/icalendar/values/helpers/array.rb0000644000004100000410000000311114715202173022641 0ustar www-datawww-data# frozen_string_literal: true module Icalendar module Values module Helpers class Array < Value attr_reader :value_delimiter def initialize(value, klass, params = {}, options = {}) @value_delimiter = options[:delimiter] || ',' mapped = if value.is_a? ::Array value.map do |v| if v.is_a? Icalendar::Values::Helpers::Array Icalendar::Values::Helpers::Array.new v.value, klass, v.ical_params, delimiter: v.value_delimiter elsif v.is_a? ::Array Icalendar::Values::Helpers::Array.new v, klass, params, delimiter: value_delimiter elsif v.is_a? Icalendar::Value v else klass.new v, params end end else [klass.new(value, params)] end super mapped end def params_ical value.each do |v| ical_params.merge! v.ical_params end super end def value_ical value.map do |v| v.value_ical end.join value_delimiter end def valid? klass = value.first.class !value.all? { |v| v.class == klass } end def value_type value.first.value_type end private def needs_value_type?(default_type) value.first.class != default_type end end end end end icalendar-2.10.3/lib/icalendar/values/boolean.rb0000644000004100000410000000042714715202173021507 0ustar www-datawww-data# frozen_string_literal: true module Icalendar module Values class Boolean < Value def initialize(value, params = {}) super value.to_s.downcase == 'true', params end def value_ical value ? 'TRUE' : 'FALSE' end end end endicalendar-2.10.3/lib/icalendar/values/cal_address.rb0000644000004100000410000000015614715202173022333 0ustar www-datawww-data# frozen_string_literal: true module Icalendar module Values class CalAddress < Uri end end endicalendar-2.10.3/lib/icalendar/values/time.rb0000644000004100000410000000125414715202173021025 0ustar www-datawww-data# frozen_string_literal: true require 'date' require_relative 'helpers/time_with_zone' module Icalendar module Values class Time < Value include Helpers::TimeWithZone FORMAT = '%H%M%S' def initialize(value, params = {}) if value.is_a? String params['tzid'] = 'UTC' if value.end_with? 'Z' super ::DateTime.strptime(value, FORMAT).to_time, params elsif value.respond_to? :to_time super value.to_time, params else super end end def value_ical if tz_utc "#{strftime FORMAT}Z" else strftime FORMAT end end end end endicalendar-2.10.3/lib/icalendar/values/binary.rb0000644000004100000410000000114014715202173021345 0ustar www-datawww-data# frozen_string_literal: true require 'base64' module Icalendar module Values class Binary < Value def params_ical ical_param :value, 'BINARY' ical_param :encoding, 'BASE64' super end def value_ical if base64? value else Base64.strict_encode64 value end end private 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 def base64? value.is_a?(String) && value =~ BASE_64_REGEX end end end endicalendar-2.10.3/lib/icalendar/values/date_time.rb0000644000004100000410000000231714715202173022023 0ustar www-datawww-data# frozen_string_literal: true require 'date' require_relative 'helpers/time_with_zone' module Icalendar module Values class DateTime < Value include Helpers::TimeWithZone FORMAT = '%Y%m%dT%H%M%S' def initialize(value, params = {}) if value.is_a? String params['tzid'] = 'UTC' if value.end_with? 'Z' begin parsed_date = ::DateTime.strptime(value, FORMAT) rescue ArgumentError => e raise FormatError.new("Failed to parse \"#{value}\" - #{e.message}") end super parsed_date, params elsif value.respond_to? :to_datetime super value.to_datetime, params else super end end def value_ical if tz_utc "#{strftime FORMAT}Z" else strftime FORMAT end end def <=>(other) if other.is_a?(Icalendar::Values::Date) || other.is_a?(Icalendar::Values::DateTime) value_ical <=> other.value_ical else nil end end def utc? value.respond_to?(:utc?) ? value.utc? : value.to_time.utc? end class FormatError < ArgumentError end end end end icalendar-2.10.3/lib/icalendar/values/date_or_date_time.rb0000644000004100000410000000153714715202173023523 0ustar www-datawww-datamodule Icalendar module Values # DateOrDateTime can be used to set an attribute to either a Date or a DateTime value. # It should not be used without also invoking the `call` method. class DateOrDateTime < Value def call parsed end def value_ical parsed.value_ical end def params_ical parsed.params_ical end private def parsed @parsed ||= begin Icalendar::Values::DateTime.new value, ical_params rescue Icalendar::Values::DateTime::FormatError Icalendar::Values::Date.new value, ical_params end end def needs_value_type?(default_type) parsed.class != default_type end def value_type parsed.class.value_type end end end end icalendar-2.10.3/lib/icalendar/values/uri.rb0000644000004100000410000000045414715202173020667 0ustar www-datawww-data# frozen_string_literal: true require 'uri' module Icalendar module Values class Uri < Value def initialize(value, params = {}) parsed = URI.parse(value) rescue value super parsed, params end def value_ical value.to_s end end end end icalendar-2.10.3/lib/icalendar/event.rb0000644000004100000410000000425514715202173017715 0ustar www-datawww-data# frozen_string_literal: true module Icalendar class Event < Component required_property :dtstamp, Icalendar::Values::DateTime required_property :uid # dtstart only required if calendar's method is nil required_property :dtstart, Icalendar::Values::DateTime, ->(event, dtstart) { !dtstart.nil? || !(event.parent.nil? || event.parent.ip_method.nil?) } optional_single_property :dtend, Icalendar::Values::DateTime optional_single_property :duration, Icalendar::Values::Duration mutually_exclusive_properties :dtend, :duration optional_single_property :ip_class optional_single_property :color optional_single_property :created, Icalendar::Values::DateTime optional_single_property :description optional_single_property :geo, Icalendar::Values::Float optional_single_property :last_modified, Icalendar::Values::DateTime optional_single_property :location optional_single_property :organizer, Icalendar::Values::CalAddress optional_single_property :priority, Icalendar::Values::Integer optional_single_property :sequence, Icalendar::Values::Integer optional_single_property :status optional_single_property :summary optional_single_property :transp optional_single_property :url, Icalendar::Values::Uri, true optional_single_property :recurrence_id, Icalendar::Values::DateTime optional_property :rrule, Icalendar::Values::Recur, true optional_property :attach, Icalendar::Values::Uri optional_property :attendee, Icalendar::Values::CalAddress optional_property :categories optional_property :comment optional_property :contact optional_property :exdate, Icalendar::Values::DateTime optional_property :request_status optional_property :related_to optional_property :resources optional_property :rdate, Icalendar::Values::DateTime optional_property :conference, Icalendar::Values::Uri, false, true optional_property :image, Icalendar::Values::Uri, false, true component :alarm, false def initialize super 'event' self.dtstamp = Icalendar::Values::DateTime.new Time.now.utc, 'tzid' => 'UTC' self.uid = new_uid end end end icalendar-2.10.3/lib/icalendar/marshable.rb0000644000004100000410000000146514715202173020532 0ustar www-datawww-data# frozen_string_literal: true module Icalendar module Marshable def self.included(base) base.extend ClassMethods end def marshal_dump instance_variables .reject { |ivar| self.class.transient_variables.include?(ivar) } .each_with_object({}) do |ivar, serialized| serialized[ivar] = instance_variable_get(ivar) end end def marshal_load(serialized) serialized.each do |ivar, value| unless self.class.transient_variables.include?(ivar) instance_variable_set(ivar, value) end end end module ClassMethods def transient_variables @transient_variables ||= [:@transient_variables] end def transient_variable(name) transient_variables.push(name.to_sym) end end end end icalendar-2.10.3/lib/icalendar/has_properties.rb0000644000004100000410000001255514715202173021625 0ustar www-datawww-data# frozen_string_literal: true module Icalendar module HasProperties def self.included(base) base.extend ClassMethods base.class_eval do attr_reader :custom_properties end end def initialize(*args) @custom_properties = Hash.new super end def valid?(strict = false) self.class.required_properties.each_pair do |prop, validator| validator.call(self, send(prop)) or return false end self.class.mutex_properties.each do |mutexprops| mutexprops.map { |p| send p }.compact.size > 1 and return false end if strict self.class.suggested_single_properties.each do |single_prop| send(single_prop).size > 1 and return false end end true end def property(property_name) property_name = property_name.downcase if self.class.properties.include? property_name send property_name else custom_property property_name end end def custom_property(property_name) custom_properties[property_name.downcase] || [] end def append_custom_property(property_name, value) property_name = property_name.downcase if self.class.single_properties.include? property_name send "#{property_name}=", value elsif self.class.multiple_properties.include? property_name send "append_#{property_name}", value elsif value.is_a? Icalendar::Value (custom_properties[property_name] ||= []) << value else (custom_properties[property_name] ||= []) << Icalendar::Values::Text.new(value) end end def method_missing(method, *args, &block) method_name = method.to_s if method_name.start_with? 'x_' if method_name.end_with? '=' append_custom_property method_name.chomp('='), args.first else custom_property method_name end else super end end def respond_to_missing?(method, include_private = false) method.to_s.start_with?('x_') || super end module ClassMethods def properties single_properties + multiple_properties end def single_properties @single_properties ||= [] end def multiple_properties @multiple_properties ||= [] end def required_properties @required_properties ||= {} end def suggested_single_properties @suggested_single_properties ||= [] end def mutex_properties @mutex_properties ||= [] end def default_property_types @default_property_types ||= Hash.new { |h,k| Icalendar::Values::Text } end def required_property(prop, klass = Icalendar::Values::Text, validator = nil) validator ||= ->(component, value) { !value.nil? } self.required_properties[prop] = validator single_property prop, klass, false end def required_multi_property(prop, klass = Icalendar::Values::Text, validator = nil) validator ||= ->(component, value) { !value.compact.empty? } self.required_properties[prop] = validator multi_property prop, klass, false end def optional_single_property(prop, klass = Icalendar::Values::Text, new_property = false) single_property prop, klass, new_property end def mutually_exclusive_properties(*properties) self.mutex_properties << properties end def optional_property(prop, klass = Icalendar::Values::Text, suggested_single = false, new_property = false) self.suggested_single_properties << prop if suggested_single multi_property prop, klass, new_property end def single_property(prop, klass, new_property) self.single_properties << prop.to_s self.default_property_types[prop.to_s] = klass define_method prop do instance_variable_get "@#{prop}" end define_method "#{prop}=" do |value| instance_variable_set "@#{prop}", map_property_value(value, klass, false, new_property) end end def multi_property(prop, klass, new_property) self.multiple_properties << prop.to_s self.default_property_types[prop.to_s] = klass property_var = "@#{prop}" define_method "#{prop}=" do |value| mapped = map_property_value value, klass, true, new_property if mapped.is_a? Icalendar::Values::Helpers::Array instance_variable_set property_var, mapped.to_a.compact else instance_variable_set property_var, [mapped].compact end end define_method prop do if instance_variable_defined? property_var instance_variable_get property_var else send "#{prop}=", nil end end define_method "append_#{prop}" do |value| send(prop) << map_property_value(value, klass, true, new_property) end end end private def map_property_value(value, klass, multi_valued, new_property) params = {} if new_property params.merge!('VALUE': klass.value_type) end if value.nil? || value.is_a?(Icalendar::Value) value elsif value.is_a? ::Array Icalendar::Values::Helpers::Array.new value, klass, params, {delimiter: (multi_valued ? ',' : ';')} else klass.new value, params end end end end icalendar-2.10.3/lib/icalendar/version.rb0000644000004100000410000000011314715202173020246 0ustar www-datawww-data# frozen_string_literal: true module Icalendar VERSION = '2.10.3' end icalendar-2.10.3/lib/icalendar/has_components.rb0000644000004100000410000000534714715202173021617 0ustar www-datawww-data# frozen_string_literal: true module Icalendar module HasComponents def self.included(base) base.extend ClassMethods base.class_eval do attr_reader :custom_components end end def initialize(*args) @custom_components = Hash.new super end def add_component(c) c.parent = self yield c if block_given? send("#{c.name.downcase}s") << c c end def add_custom_component(component_name, c) c.parent = self yield c if block_given? (custom_components[component_name.downcase.gsub("-", "_")] ||= []) << c c end def custom_component(component_name) custom_components[component_name.downcase.gsub("-", "_")] || [] end METHOD_MISSING_ADD_REGEX = /^add_(x_\w+)$/.freeze METHOD_MISSING_X_FLAG_REGEX = /^x_/.freeze def method_missing(method, *args, &block) method_name = method.to_s if method_name =~ METHOD_MISSING_ADD_REGEX component_name = $1 custom = args.first || Component.new(component_name, component_name.upcase) add_custom_component(component_name, custom, &block) elsif method_name =~ METHOD_MISSING_X_FLAG_REGEX && custom_component(method_name).size > 0 custom_component method_name else super end end def respond_to_missing?(method_name, include_private = false) string_method = method_name.to_s string_method.start_with?('add_x_') || custom_component(string_method).size > 0 || super end module ClassMethods def components @components ||= [] end def component(singular_name, find_by = :uid, klass = nil) components = "#{singular_name}s" self.components << components component_var = "@#{components}" define_method components do if instance_variable_defined? component_var instance_variable_get component_var else instance_variable_set component_var, [] end end define_method singular_name do |c = nil, &block| if c.nil? c = begin klass ||= Icalendar.const_get singular_name.capitalize klass.new rescue NameError => ne Icalendar.logger.warn ne.message Component.new singular_name end end add_component c, &block end define_method "find_#{singular_name}" do |id| send(components).find { |c| c.send(find_by) == id } end if find_by define_method "add_#{singular_name}" do |c| send singular_name, c end define_method "has_#{singular_name}?" do !send(components).empty? end end end end end icalendar-2.10.3/lib/icalendar/calendar.rb0000644000004100000410000000305414715202173020341 0ustar www-datawww-data# frozen_string_literal: true module Icalendar class Calendar < Component required_property :version required_property :prodid optional_single_property :calscale optional_single_property :ip_method optional_property :ip_name optional_property :description optional_single_property :uid optional_single_property :last_modified, Icalendar::Values::DateTime, true optional_single_property :url, Icalendar::Values::Uri, true optional_property :categories optional_single_property :refresh_interval, Icalendar::Values::Duration, true optional_single_property :source, Icalendar::Values::Uri, true optional_single_property :color optional_property :image, Icalendar::Values::Uri, false, true component :timezone, :tzid component :event component :todo component :journal component :freebusy def initialize super 'calendar' self.prodid = 'icalendar-ruby' self.version = '2.0' self.calscale = 'GREGORIAN' end def publish self.ip_method = 'PUBLISH' self end def request self.ip_method = 'REQUEST' self end def reply self.ip_method = 'REPLY' self end def add self.ip_method = 'ADD' self end def cancel self.ip_method = 'CANCEL' self end def refresh self.ip_method = 'REFRESH' self end def counter self.ip_method = 'COUNTER' self end def decline_counter self.ip_method = 'DECLINECOUNTER' self end end end icalendar-2.10.3/lib/icalendar/journal.rb0000644000004100000410000000276614715202173020253 0ustar www-datawww-data# frozen_string_literal: true module Icalendar class Journal < Component required_property :dtstamp, Icalendar::Values::DateTime required_property :uid optional_single_property :ip_class optional_single_property :color optional_single_property :created, Icalendar::Values::DateTime optional_single_property :dtstart, Icalendar::Values::DateTime optional_single_property :last_modified, Icalendar::Values::DateTime optional_single_property :organizer, Icalendar::Values::CalAddress optional_single_property :recurrence_id, Icalendar::Values::DateTime optional_single_property :sequence, Icalendar::Values::Integer optional_single_property :status optional_single_property :summary optional_single_property :url, Icalendar::Values::Uri optional_property :rrule, Icalendar::Values::Recur, true optional_property :attach, Icalendar::Values::Uri optional_property :attendee, Icalendar::Values::CalAddress optional_property :categories optional_property :comment optional_property :contact optional_property :description optional_property :exdate, Icalendar::Values::DateTime optional_property :request_status optional_property :related_to optional_property :rdate, Icalendar::Values::DateTime optional_property :image, Icalendar::Values::Uri, false, true def initialize super 'journal' self.dtstamp = Icalendar::Values::DateTime.new Time.now.utc, 'tzid' => 'UTC' self.uid = new_uid end end endicalendar-2.10.3/lib/icalendar/component.rb0000644000004100000410000000663414715202173020601 0ustar www-datawww-data# frozen_string_literal: true require 'securerandom' module Icalendar class Component include HasProperties include HasComponents attr_reader :name attr_reader :ical_name attr_accessor :parent def self.parse(source) _parse source rescue ArgumentError source.rewind if source.respond_to?(:rewind) _parse Parser.clean_bad_wrapping(source) end def initialize(name, ical_name = nil) @name = name @ical_name = ical_name || "V#{name.upcase}" super() end def new_uid SecureRandom.uuid end def to_ical [ "BEGIN:#{ical_name}", ical_properties, ical_components, "END:#{ical_name}\r\n" ].compact.join "\r\n" end private def ical_properties (self.class.properties + custom_properties.keys).map do |prop| value = property prop unless value.nil? if value.is_a? ::Array value.map do |part| ical_fold "#{ical_prop_name prop}#{part.to_ical self.class.default_property_types[prop]}" end.join "\r\n" unless value.empty? else ical_fold "#{ical_prop_name prop}#{value.to_ical self.class.default_property_types[prop]}" end end end.compact.join "\r\n" end ICAL_PROP_NAME_GSUB_REGEX = /\Aip_/.freeze def ical_prop_name(prop_name) prop_name.gsub(ICAL_PROP_NAME_GSUB_REGEX, '').gsub('_', '-').upcase end ICAL_FOLD_LONG_LINE_SCAN_REGEX = /\P{M}\p{M}*/u.freeze def ical_fold(long_line, indent = "\x20") # rfc2445 says: # Lines of text SHOULD NOT be longer than 75 octets, excluding the line # break. Long content lines SHOULD be split into a multiple line # representations using a line "folding" technique. That is, a long # line can be split between any two characters by inserting a CRLF # immediately followed by a single linear white space character (i.e., # SPACE, US-ASCII decimal 32 or HTAB, US-ASCII decimal 9). Any sequence # of CRLF followed immediately by a single linear white space character # is ignored (i.e., removed) when processing the content type. # # Note the useage of "octets" and "characters": a line should not be longer # than 75 octets, but you need to split between characters, not bytes. # This is challanging with Unicode composing accents, for example. return long_line if long_line.bytesize <= Icalendar::MAX_LINE_LENGTH chars = long_line.scan(ICAL_FOLD_LONG_LINE_SCAN_REGEX) # split in graphenes folded = [''] bytes = 0 while chars.count > 0 c = chars.shift cb = c.bytes.count if bytes + cb > Icalendar::MAX_LINE_LENGTH # Split here folded.push "#{indent}" bytes = indent.bytes.count end folded[-1] += c bytes += cb end folded.join("\r\n") end def ical_components collection = [] (self.class.components + custom_components.keys).each do |component_name| components = send component_name components.each do |component| collection << component.to_ical end end collection.empty? ? nil : collection.join.chomp("\r\n") end class << self private def _parse(source) parser = Parser.new(source) parser.component_class = self parser.parse end end end end icalendar-2.10.3/lib/icalendar/value.rb0000644000004100000410000000450514715202173017706 0ustar www-datawww-data# frozen_string_literal: true require 'delegate' require 'icalendar/downcased_hash' module Icalendar class Value < ::SimpleDelegator attr_accessor :ical_params def initialize(value, params = {}) @ical_params = Icalendar::DowncasedHash(params) super value end def ical_param(key, value) @ical_params[key] = value end def value __getobj__ end def to_ical(default_type) ical_param 'value', self.value_type if needs_value_type?(default_type) "#{params_ical}:#{value_ical}" end def params_ical unless ical_params.empty? ";#{ical_params.map { |name, value| param_ical name, value }.join ';'}" end end VALUE_TYPE_GSUB_REGEX_1 = /\A.*::/.freeze VALUE_TYPE_GSUB_REGEX_2 = /(? ddst standard.last.tzoffsetto else daylight.last.tzoffsetto end end end def standard_for(local) possible = standards.map do |std| prev = std.previous_occurrence(local.to_time) [prev, std] unless prev.nil? end.compact possible.sort_by(&:first).last end def daylight_for(local) possible = daylights.map do |day| prev = day.previous_occurrence(local.to_time) [prev, day] unless prev.nil? end.compact possible.sort_by(&:first).last end end end icalendar-2.10.3/lib/icalendar/timezone_store.rb0000644000004100000410000000160214715202173021633 0ustar www-datawww-data# frozen_string_literal: true require 'delegate' require 'icalendar/downcased_hash' module Icalendar class TimezoneStore < ::SimpleDelegator def initialize super DowncasedHash.new({}) end def self.instance warn "**** DEPRECATION WARNING ****\nTimezoneStore.instance will be removed in 3.0. Please instantiate a TimezoneStore object." @instance ||= new end def self.store(timezone) warn "**** DEPRECATION WARNING ****\nTimezoneStore.store will be removed in 3.0. Please use instance methods." instance.store timezone end def self.retrieve(tzid) warn "**** DEPRECATION WARNING ****\nTimezoneStore.retrieve will be removed in 3.0. Please use instance methods." instance.retrieve tzid end def store(timezone) self[timezone.tzid] = timezone end def retrieve(tzid) self[tzid] end end end icalendar-2.10.3/lib/icalendar/parser.rb0000644000004100000410000001721214715202173020065 0ustar www-datawww-data# frozen_string_literal: true require 'icalendar/timezone_store' require 'stringio' module Icalendar class Parser attr_writer :component_class attr_reader :source, :strict, :timezone_store, :verbose CLEAN_BAD_WRAPPING_GSUB_REGEX = /\r?\n[ \t]/.freeze def self.clean_bad_wrapping(source) content = if source.respond_to? :read source.read elsif source.respond_to? :to_s source.to_s else msg = 'Icalendar::Parser.clean_bad_wrapping must be called with a String or IO object' Icalendar.fatal msg fail ArgumentError, msg end encoding = content.encoding content.force_encoding(Encoding::ASCII_8BIT) content.gsub(CLEAN_BAD_WRAPPING_GSUB_REGEX, "").force_encoding(encoding) end def initialize(source, strict = false, verbose = false) if source.respond_to? :gets @source = source elsif source.respond_to? :to_s @source = StringIO.new source.to_s, 'r' else msg = 'Icalendar::Parser.new must be called with a String or IO object' Icalendar.fatal msg fail ArgumentError, msg end read_in_data @strict = strict @verbose = verbose @timezone_store = TimezoneStore.new end def parse components = [] while (fields = next_fields) component = component_class.new if fields[:name] == 'begin' && fields[:value].downcase == component.ical_name.downcase components << parse_component(component) end end components end def parse_property(component, fields = nil) fields = next_fields if fields.nil? prop_name = %w(class method name).include?(fields[:name]) ? "ip_#{fields[:name]}" : fields[:name] multi_property = component.class.multiple_properties.include? prop_name prop_value = wrap_property_value component, fields, multi_property begin method_name = if multi_property "append_#{prop_name}" else "#{prop_name}=" end component.send method_name, prop_value rescue NoMethodError => nme if strict? Icalendar.logger.error "No method \"#{method_name}\" for component #{component}" raise nme else Icalendar.logger.warn "No method \"#{method_name}\" for component #{component}. Appending to custom." if verbose? component.append_custom_property prop_name, prop_value end end end WRAP_PROPERTY_VALUE_DELIMETER_REGEX = /(? fe raise fe if strict? fields[:params]['value'] = ['DATE'] retry end WRAP_IN_ARRAY_REGEX_1 = /(?#{NAME})(?(?:;#{PARAM})*):(?#{VALUE})" BAD_LINE = "(?#{NAME})(?(?:;#{PARAM})*)" LINE_REGEX = %r{#{LINE}}.freeze BAD_LINE_REGEX = %r{#{BAD_LINE}}.freeze PARAM_REGEX = %r{#{PARAM}}.freeze PVALUE_REGEX = %r{#{PVALUE}}.freeze PVALUE_GSUB_REGEX = /\A"|"\z/.freeze def parse_fields(input) if parts = LINE_REGEX.match(input) value = parts[:value] else parts = BAD_LINE_REGEX.match(input) unless strict? parts or fail ParseError, "Invalid iCalendar input line: #{input}" # Non-strict and bad line so use a value of empty string value = '' end params = {} parts[:params].scan PARAM_REGEX do |match| param_name = match[0].downcase params[param_name] ||= [] match[1].scan PVALUE_REGEX do |param_value| if param_value.size > 0 param_value = param_value.gsub(PVALUE_GSUB_REGEX, '') params[param_name] << param_value if param_name == 'tzid' params['x-tz-store'] = timezone_store end end end end # Building the string to send to the logger is expensive. # Only do it if the logger is at the right log level. if ::Logger::DEBUG >= Icalendar.logger.level Icalendar.logger.debug "Found fields: #{parts.inspect} with params: #{params.inspect}" end { name: parts[:name].downcase.gsub('-', '_'), params: params, value: value } end class ParseError < RuntimeError end end end icalendar-2.10.3/lib/icalendar.rb0000644000004100000410000000157314715202173016574 0ustar www-datawww-data# frozen_string_literal: true require 'icalendar/logger' module Icalendar MAX_LINE_LENGTH = 75 def self.logger @logger ||= Icalendar::Logger.new(STDERR) end def self.logger=(logger) @logger = logger end def self.parse(source, single = false) warn "**** DEPRECATION WARNING ****\nIcalendar.parse will be removed in 3.0. Please switch to Icalendar::Calendar.parse." calendars = Parser.new(source).parse single ? calendars.first : calendars end end require 'icalendar/has_properties' require 'icalendar/has_components' require 'icalendar/marshable' require 'icalendar/component' require 'icalendar/value' require 'icalendar/alarm' require 'icalendar/event' require 'icalendar/todo' require 'icalendar/journal' require 'icalendar/freebusy' require 'icalendar/timezone' require 'icalendar/calendar' require 'icalendar/parser' require 'icalendar/version' icalendar-2.10.3/spec/0000755000004100000410000000000014715202173014503 5ustar www-datawww-dataicalendar-2.10.3/spec/spec_helper.rb0000644000004100000410000000202214715202173017315 0ustar www-datawww-datarequire 'simplecov' SimpleCov.start do add_filter '/spec/' end # This file was generated by the `rspec --init` command. Conventionally, all # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. # Require this file using `require "spec_helper"` to ensure that it is only # loaded once. # # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration require 'timecop' require 'icalendar' if defined?(ActiveSupport) # this has been default behavior for new Rails apps for a long time, and the # only option once ActiveSupport goes to 8.x ActiveSupport.to_time_preserves_timezone = true if ActiveSupport.respond_to?(:to_time_preserves_timezone=) end RSpec.configure do |config| config.run_all_when_everything_filtered = true config.filter_run :focus # Run specs in random order to surface order dependencies. If you find an # order dependency and want to debug it, you can fix the order by providing # the seed, which is printed after each run. # --seed 1234 config.order = 'random' end icalendar-2.10.3/spec/tzinfo_spec.rb0000644000004100000410000000657514715202173017370 0ustar www-datawww-datarequire 'spec_helper' require_relative '../lib/icalendar/tzinfo' describe 'TZInfo::Timezone' do let(:tz) { TZInfo::Timezone.get 'Europe/Copenhagen' } let(:date) { DateTime.new 2014 } subject { tz.ical_timezone date } describe 'daylight offset' do specify { expect(subject.daylights.first.tzoffsetto.value_ical).to eq "+0200" } specify { expect(subject.daylights.first.tzoffsetfrom.value_ical).to eq "+0100" } end describe 'standard offset' do specify { expect(subject.standards.first.tzoffsetto.value_ical).to eq "+0100" } specify { expect(subject.standards.first.tzoffsetfrom.value_ical).to eq "+0200" } end describe 'daylight recurrence rule' do specify { expect(subject.daylights.first.rrule.first.value_ical).to eq "FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3" } end describe 'standard recurrence rule' do specify { expect(subject.standards.first.rrule.first.value_ical).to eq "FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10" } end describe 'no end transition' do let(:tz) { TZInfo::Timezone.get 'Asia/Shanghai' } let(:date) { DateTime.now } it 'only creates a standard component' do expect(subject.to_ical).to eq <<-EXPECTED.gsub "\n", "\r\n" BEGIN:VTIMEZONE TZID:Asia/Shanghai BEGIN:STANDARD DTSTART:19910915T010000 TZOFFSETFROM:+0900 TZOFFSETTO:+0800 TZNAME:CST END:STANDARD END:VTIMEZONE EXPECTED end end describe 'no transition' do let(:tz) { TZInfo::Timezone.get 'UTC' } let(:date) { DateTime.now } it 'creates a standard component with equal offsets' do expect(subject.to_ical).to eq <<-EXPECTED.gsub "\n", "\r\n" BEGIN:VTIMEZONE TZID:UTC BEGIN:STANDARD DTSTART:19700101T000000 TZOFFSETFROM:+0000 TZOFFSETTO:+0000 TZNAME:UTC END:STANDARD END:VTIMEZONE EXPECTED end end describe 'dst transition' do subject { TZInfo::Timezone.get 'America/Los_Angeles' } let(:now) { subject.now } # freeze in DST transition in America/Los_Angeles before(:each) { Timecop.freeze DateTime.new(2013, 11, 03, 1, 30, 0, '-08:00') } after(:each) { Timecop.return } specify { expect { subject.ical_timezone now, nil }.to raise_error TZInfo::AmbiguousTime } specify { expect { subject.ical_timezone now, true }.not_to raise_error } specify { expect { subject.ical_timezone now, false }.not_to raise_error } context 'TZInfo::Timezone.default_dst = nil' do before(:each) { TZInfo::Timezone.default_dst = nil } specify { expect { subject.ical_timezone now }.to raise_error TZInfo::AmbiguousTime } end context 'TZInfo::Timezone.default_dst = true' do before(:each) { TZInfo::Timezone.default_dst = true } specify { expect { subject.ical_timezone now }.not_to raise_error } end context 'TZInfo::Timezone.default_dst = false' do before(:each) { TZInfo::Timezone.default_dst = false } specify { expect { subject.ical_timezone now }.not_to raise_error } end end describe 'tzname for offset' do # Check for CET/CEST correctness, which doesn't follow # the more common *ST/*DT style abbreviations. let(:tz) { TZInfo::Timezone.get 'Europe/Prague' } let(:ical_tz) { tz.ical_timezone date } describe '#daylight' do subject(:tzname) { ical_tz.daylights.first.tzname.first } it { should eql 'CEST' } end describe '#standard' do subject(:tzname) { ical_tz.standards.first.tzname.first } it { should eql 'CET' } end end end icalendar-2.10.3/spec/journal_spec.rb0000644000004100000410000000017014715202173017512 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Journal do # currently no behavior in Journal not tested other places endicalendar-2.10.3/spec/calendar_spec.rb0000644000004100000410000001543114715202173017617 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Calendar do context 'marshalling' do it 'can be de/serialized' do Marshal.load(Marshal.dump(subject)) end end context 'values' do let(:property) { 'my-value' } %w(prodid version calscale ip_method).each do |prop| it "##{prop} sets and gets" do subject.send("#{prop}=", property) expect(subject.send prop).to eq property end end it "sets and gets custom properties" do subject.x_custom_prop = property expect(subject.x_custom_prop).to eq [property] end it 'can set params on a property' do subject.prodid.ical_params = {'hello' => 'world'} expect(subject.prodid.value).to eq 'icalendar-ruby' expect(subject.prodid.ical_params).to eq('hello' => 'world') end context "required values" do it 'is not valid when prodid is not set' do subject.prodid = nil expect(subject).to_not be_valid end it 'is not valid when version is not set' do subject.version = nil expect(subject).to_not be_valid end it 'is valid when both prodid and version are set' do subject.version = '2.0' subject.prodid = 'my-product' expect(subject).to be_valid end it 'is valid by default' do expect(subject).to be_valid end end end context 'components' do let(:ical_component) { double 'Component', name: 'event', :'parent=' => nil } %w(event todo journal freebusy timezone).each do |component| it "##{component} adds a new component" do expect(subject.send "#{component}").to be_a_kind_of Icalendar::Component end it "##{component} passes a component to a block to build parts" do expect { |b| subject.send("#{component}", &b) }.to yield_with_args Icalendar::Component end it "##{component} can be passed in" do expect { |b| subject.send("#{component}", ical_component, &b) }.to yield_with_args ical_component expect(subject.send "#{component}", ical_component).to eq ical_component end end it "adds event to events list" do subject.event ical_component expect(subject.events).to eq [ical_component] end describe '#add_event' do it 'delegates to non add_ version' do expect(subject).to receive(:event).with(ical_component) subject.add_event ical_component end end describe '#find_event' do let(:ical_component) { double 'Component', uid: 'uid' } let(:other_component) { double 'Component', uid: 'other' } before(:each) do subject.events << other_component subject.events << ical_component end it 'finds by uid' do expect(subject.find_event 'uid').to eq ical_component end end describe '#find_timezone' do let(:ical_timezone) { double 'Timezone', tzid: 'Eastern' } let(:other_timezone) { double 'Timezone', tzid: 'Pacific' } before(:each) do subject.timezones << other_timezone subject.timezones << ical_timezone end it 'finds by tzid' do expect(subject.find_timezone 'Eastern').to eq ical_timezone end end it "adds reference to parent" do e = subject.event expect(e.parent).to eq subject end it 'can be added with add_x_ for custom components' do expect(subject.add_x_custom_component).to be_a_kind_of Icalendar::Component expect { |b| subject.add_x_custom_component(&b) }.to yield_with_args Icalendar::Component expect(subject.add_x_custom_component ical_component).to eq ical_component end end describe '#to_ical' do before(:each) do Timecop.freeze DateTime.new(2013, 12, 26, 5, 0, 0, '+0000') subject.ip_name = 'Company Vacation Days' subject.description = 'The description' subject.last_modified = "20140101T000000Z" subject.url = 'https://example.com' subject.color = 'red' subject.image = 'https://example.com/image.png' subject.uid = '5FC53010-1267-4F8E-BC28-1D7AE55A7C99' subject.categories = 'MEETING' subject.refresh_interval = 'P1W' subject.source = 'https://example.com/holidays.ics' subject.event do |e| e.summary = 'An event' e.dtstart = "20140101T000000Z" e.dtend = "20140101T050000Z" e.geo = [-1.2, -2.1] end subject.freebusy do |f| f.dtstart = "20140102T080000Z" f.dtend = "20140102T100000Z" f.comment = 'Busy' end end after(:each) do Timecop.return end it 'outputs properties and components' do expected_no_uid = <<-EOICAL.gsub("\n", "\r\n") BEGIN:VCALENDAR VERSION:2.0 PRODID:icalendar-ruby CALSCALE:GREGORIAN LAST-MODIFIED;VALUE=DATE-TIME:20140101T000000Z URL;VALUE=URI:https://example.com REFRESH-INTERVAL;VALUE=DURATION:P1W SOURCE;VALUE=URI:https://example.com/holidays.ics COLOR:red NAME:Company Vacation Days DESCRIPTION:The description CATEGORIES:MEETING IMAGE;VALUE=URI:https://example.com/image.png BEGIN:VEVENT DTSTAMP:20131226T050000Z DTSTART:20140101T000000Z DTEND:20140101T050000Z GEO:-1.2;-2.1 SUMMARY:An event END:VEVENT BEGIN:VFREEBUSY DTSTAMP:20131226T050000Z DTSTART:20140102T080000Z DTEND:20140102T100000Z COMMENT:Busy END:VFREEBUSY END:VCALENDAR EOICAL expect(subject.to_ical.gsub(/^UID:.*\r\n(?: .*\r\n)*/, '')).to eq expected_no_uid end end describe '#publish' do it 'sets ip_method to "PUBLISH"' do subject.publish expect(subject.ip_method).to eq 'PUBLISH' end end describe '#request' do it 'sets ip_method to "REQUEST"' do subject.request expect(subject.ip_method).to eq 'REQUEST' end end describe '#reply' do it 'sets ip_method to "REPLY"' do subject.reply expect(subject.ip_method).to eq 'REPLY' end end describe '#add' do it 'sets ip_method to "ADD"' do subject.add expect(subject.ip_method).to eq 'ADD' end end describe '#cancel' do it 'sets ip_method to "CANCEL"' do subject.cancel expect(subject.ip_method).to eq 'CANCEL' end end describe '#refresh' do it 'sets ip_method to "REFRESH"' do subject.refresh expect(subject.ip_method).to eq 'REFRESH' end end describe '#counter' do it 'sets ip_method to "COUNTER"' do subject.counter expect(subject.ip_method).to eq 'COUNTER' end end describe '#decline_counter' do it 'sets ip_method to "DECLINECOUNTER"' do subject.decline_counter expect(subject.ip_method).to eq 'DECLINECOUNTER' end end describe '.parse' do let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'bad_wrapping.ics') } it 'correctly parses a bad file' do actual = described_class.parse(source) expect(actual[0]).to be_a(described_class) end end end icalendar-2.10.3/spec/downcased_hash_spec.rb0000644000004100000410000000240414715202173021014 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::DowncasedHash do subject { described_class.new base } let(:base) { {'hello' => 'world'} } describe '#[]=' do it 'sets a new value' do subject['FOO'] = 'bar' expect(subject['foo']).to eq 'bar' end end describe '#[]' do it 'gets an already set value' do subject['foo'] = 'bar' expect(subject['FOO']).to eq 'bar' end end describe '#has_key?' do it 'correctly identifies keys in the hash' do expect(subject.has_key? 'hello').to be true expect(subject.has_key? 'HELLO').to be true end end describe '#delete' do context 'no block' do it 'removes the key' do subject.delete 'HELLO' expect(subject.has_key? 'hello').to be false end end context 'with a block' do it 'calls the block when the key is not found' do expect { |b| subject.delete 'nokey', &b }.to yield_with_args('nokey') end end end describe 'DowncasedHash()' do it 'returns self when passed an DowncasedHash' do expect(Icalendar::DowncasedHash(subject)).to be subject end it 'wraps a hash in an downcased hash' do expect(Icalendar::DowncasedHash(base)).to be_kind_of Icalendar::DowncasedHash end end end icalendar-2.10.3/spec/fixtures/0000755000004100000410000000000014715202173016354 5ustar www-datawww-dataicalendar-2.10.3/spec/fixtures/custom_component.ics0000644000004100000410000001000214715202173022441 0ustar www-datawww-dataBEGIN:VCALENDAR PRODID:-//Atlassian Confluence//Calendar Plugin 1.0//EN VERSION:2.0 CALSCALE:GREGORIAN X-WR-CALNAME:Grimsell testkalender X-WR-CALDESC: X-WR-TIMEZONE:Europe/Stockholm X-MIGRATED-FOR-USER-KEY:true METHOD:PUBLISH BEGIN:VTIMEZONE TZID:Europe/Stockholm TZURL:http://tzurl.org/zoneinfo/Europe/Stockholm SEQUENCE:9206 BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19810329T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19961027T030000 RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU END:STANDARD BEGIN:STANDARD TZOFFSETFROM:+011212 TZOFFSETTO:+010014 TZNAME:SET DTSTART:18790101T000000 RDATE:18790101T000000 END:STANDARD BEGIN:STANDARD TZOFFSETFROM:+010014 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19000101T000000 RDATE:19000101T000000 END:STANDARD BEGIN:DAYLIGHT TZOFFSETFROM:+0100 TZOFFSETTO:+0200 TZNAME:CEST DTSTART:19160514T230000 RDATE:19160514T230000 RDATE:19800406T020000 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:+0200 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19161001T010000 RDATE:19161001T010000 RDATE:19800928T030000 RDATE:19810927T030000 RDATE:19820926T030000 RDATE:19830925T030000 RDATE:19840930T030000 RDATE:19850929T030000 RDATE:19860928T030000 RDATE:19870927T030000 RDATE:19880925T030000 RDATE:19890924T030000 RDATE:19900930T030000 RDATE:19910929T030000 RDATE:19920927T030000 RDATE:19930926T030000 RDATE:19940925T030000 RDATE:19950924T030000 END:STANDARD BEGIN:STANDARD TZOFFSETFROM:+0100 TZOFFSETTO:+0100 TZNAME:CET DTSTART:19800101T000000 RDATE:19800101T000000 END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTAMP:20151203T104347Z SUMMARY:Kaffepaus UID:20151203T101856Z-1543254179@intranet.idainfront.se DESCRIPTION: ORGANIZER;X-CONFLUENCE-USER-KEY=ff80818141e4d3b60141e4d4b75700a2;CN=Magnu s Grimsell;CUTYPE=INDIVIDUAL:mailto:magnus.grimsell@idainfront.se CREATED:20151203T101856Z LAST-MODIFIED:20151203T101856Z SEQUENCE:1 X-CONFLUENCE-SUBCALENDAR-TYPE:other TRANSP:OPAQUE STATUS:CONFIRMED DTSTART:20151203T090000Z DTEND:20151203T091500Z END:VEVENT BEGIN:X-EVENT-SERIES SUMMARY:iipax - Sprints DESCRIPTION: X-CONFLUENCE-SUBCALENDAR-TYPE:jira-agile-sprint URL:jira://8112ef9a-343e-30e6-b469-4f993bf0371d?projectKey=II&dateFieldNa me=sprint END:X-EVENT-SERIES BEGIN:VEVENT DTSTAMP:20151203T104348Z DTSTART;TZID=Europe/Stockholm:20130115T170800 DTEND;TZID=Europe/Stockholm:20130204T170800 UID:3cb4df4b-eb18-43fd-934a-16c15acfb4b1-3@jira.idainfront.se X-GREENHOPPER-SPRINT-CLOSED:false X-GREENHOPPER-SPRINT-EDITABLE:true X-JIRA-PROJECT;X-JIRA-PROJECT-KEY=II:iipax X-GREENHOPPER-SPRINT-VIEWBOARDS-URL:https://jira.idainfront.se/secure/GHG oToBoard.jspa?sprintId=3 SEQUENCE:0 X-CONFLUENCE-SUBCALENDAR-TYPE:jira-agile-sprint TRANSP:OPAQUE STATUS:CONFIRMED DESCRIPTION:https://jira.idainfront.se/secure/GHGoToBoard.jspa?sprintId=3 SUMMARY:iipax - Callisto-6 END:VEVENT BEGIN:VEVENT DTSTAMP:20151203T104348Z DTSTART;TZID=Europe/Stockholm:20130219T080000 DTEND;TZID=Europe/Stockholm:20130319T095900 UID:3cb4df4b-eb18-43fd-934a-16c15acfb4b1-5@jira.idainfront.se X-GREENHOPPER-SPRINT-CLOSED:false X-GREENHOPPER-SPRINT-EDITABLE:true X-JIRA-PROJECT;X-JIRA-PROJECT-KEY=II:iipax X-GREENHOPPER-SPRINT-VIEWBOARDS-URL:https://jira.idainfront.se/secure/GHG oToBoard.jspa?sprintId=5 SEQUENCE:0 X-CONFLUENCE-SUBCALENDAR-TYPE:jira-agile-sprint TRANSP:OPAQUE STATUS:CONFIRMED DESCRIPTION:https://jira.idainfront.se/secure/GHGoToBoard.jspa?sprintId=5 SUMMARY:iipax - Bulbasaur END:VEVENT BEGIN:VEVENT DTSTAMP:20151203T104348Z DTSTART;TZID=Europe/Stockholm:20130326T105900 DTEND;TZID=Europe/Stockholm:20130419T105900 UID:3cb4df4b-eb18-43fd-934a-16c15acfb4b1-7@jira.idainfront.se X-GREENHOPPER-SPRINT-CLOSED:true X-GREENHOPPER-SPRINT-EDITABLE:true X-JIRA-PROJECT;X-JIRA-PROJECT-KEY=II:iipax X-GREENHOPPER-SPRINT-VIEWBOARDS-URL:https://jira.idainfront.se/secure/GHG oToBoard.jspa?sprintId=7 SEQUENCE:0 X-CONFLUENCE-SUBCALENDAR-TYPE:jira-agile-sprint TRANSP:OPAQUE STATUS:CONFIRMED DESCRIPTION:https://jira.idainfront.se/secure/GHGoToBoard.jspa?sprintId=7 SUMMARY:iipax - Ivysaur END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/nonstandard.ics0000644000004100000410000000114614715202173021371 0ustar www-datawww-dataBEGIN:VCALENDAR VERSION:2.0 PRODID:bsprodidfortestabc123 BEGIN:VEVENT UID:bsuidfortestabc123 ORGANIZER:mailto:joebob@random.net ATTACH:http://bush.sucks.org/impeach/him.rhtml ATTACH:http://corporations-dominate.existence.net/why.rhtml SUMMARY:This is a really long summary to test the method of unfolding lines\, so I'm just going to ma ke it a whol e bunch of lines. CLASS:PRIVATE PRIORITY:2 CUSTOMFIELD:Not properly noted as custom with X- prefix. GEO:37.386013;-122.0829322 DTSTART;TZID=US-Mountain:20050120T170000 DTEND:20050120T184500 DTSTAMP:20050118T211523Z NAME:SomeName END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/reversed_timezone.ics0000644000004100000410000000607414715202173022614 0ustar www-datawww-dataBEGIN:VCALENDAR VERSION:2.0 X-WR-CALNAME:My Calendar X-APPLE-CALENDAR-COLOR:#44A703 BEGIN:VEVENT CREATED:20231026T211915Z DTEND;TZID=NONSTANDARDTZ:20231025T180000 DTSTAMP:20231026T211916Z DTSTART;TZID=NONSTANDARDTZ:20231025T163000 LAST-MODIFIED:20231026T211915Z LOCATION:Somewhere SEQUENCE:0 SUMMARY:Summary summary summary UID:some-unique-uid END:VEVENT BEGIN:VTIMEZONE TZID:NONSTANDARDTZ X-LIC-LOCATION:NONSTANDARDTZ BEGIN:STANDARD DTSTART:18831118T120702 RDATE:18831118T120702 TZNAME:PST TZOFFSETFROM:-075258 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19180331T020000 RRULE:FREQ=YEARLY;UNTIL=19190330T100000Z;BYMONTH=3;BYDAY=-1SU TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:19181027T020000 RRULE:FREQ=YEARLY;UNTIL=19191026T090000Z;BYMONTH=10;BYDAY=-1SU TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19420209T020000 RDATE:19420209T020000 TZNAME:PWT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19450814T160000 RDATE:19450814T160000 TZNAME:PPT TZOFFSETFROM:-0700 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:19450930T020000 RDATE:19450930T020000 RDATE:19490101T020000 TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:STANDARD DTSTART:19460101T000000 RDATE:19460101T000000 RDATE:19670101T000000 TZNAME:PST TZOFFSETFROM:-0800 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19480314T020100 RDATE:19480314T020100 RDATE:19740106T020000 RDATE:19750223T020000 TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19500430T010000 RRULE:FREQ=YEARLY;UNTIL=19660424T090000Z;BYMONTH=4;BYDAY=-1SU TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:19500924T020000 RRULE:FREQ=YEARLY;UNTIL=19610924T090000Z;BYMONTH=9;BYDAY=-1SU TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:STANDARD DTSTART:19621028T020000 RRULE:FREQ=YEARLY;UNTIL=19661030T090000Z;BYMONTH=10;BYDAY=-1SU TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19670430T020000 RRULE:FREQ=YEARLY;UNTIL=19730429T100000Z;BYMONTH=4;BYDAY=-1SU TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:19671029T020000 RRULE:FREQ=YEARLY;UNTIL=20061029T090000Z;BYMONTH=10;BYDAY=-1SU TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD BEGIN:DAYLIGHT DTSTART:19760425T020000 RRULE:FREQ=YEARLY;UNTIL=19860427T100000Z;BYMONTH=4;BYDAY=-1SU TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:19870405T020000 RRULE:FREQ=YEARLY;UNTIL=20060402T100000Z;BYMONTH=4;BYDAY=1SU TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:DAYLIGHT DTSTART:20070311T020000 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU TZNAME:PDT TZOFFSETFROM:-0800 TZOFFSETTO:-0700 END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU TZNAME:PST TZOFFSETFROM:-0700 TZOFFSETTO:-0800 END:STANDARD END:VTIMEZONE END:VCALENDAR icalendar-2.10.3/spec/fixtures/single_event_bad_organizer.ics0000644000004100000410000000147214715202173024430 0ustar www-datawww-dataBEGIN:VCALENDAR VERSION:2.0 PRODID:bsprodidfortestabc123 CALSCALE:GREGORIAN BEGIN:VEVENT DTSTAMP:20050118T211523Z UID:bsuidfortestabc123 DTSTART;TZID=US-Mountain:20050120T170000 DTEND;TZID=US-Mountain:20050120T184500 CLASS:PRIVATE GEO:37.386013;-122.0829322 ORGANIZER;CN=Joe Bob\: Magician:mailto:joebob@random.net PRIORITY:2 SUMMARY:This is a really long summary to test the method of unfolding lines \, so I'm just going to make it a whole bunch of lines. With a twist: a " ö" takes up multiple bytes\, and should be wrapped to the next line. ATTACH:http://bush.sucks.org/impeach/him.rhtml ATTACH:http://corporations-dominate.existence.net/why.rhtml RDATE;TZID=US-Mountain:20050121T170000,20050122T170000 X-TEST-COMPONENT;QTEST="Hello, World":Shouldn't double double quotes END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/single_event_organizer_parsed.ics0000644000004100000410000000147114715202173025157 0ustar www-datawww-dataBEGIN:VCALENDAR VERSION:2.0 PRODID:bsprodidfortestabc123 CALSCALE:GREGORIAN BEGIN:VEVENT DTSTAMP:20050118T211523Z UID:bsuidfortestabc123 DTSTART;TZID=US-Mountain:20050120T170000 DTEND;TZID=US-Mountain:20050120T184500 CLASS:PRIVATE GEO:37.386013;-122.0829322 ORGANIZER;CN=Joe Bob\:magician:mailto:joebob@random.net PRIORITY:2 SUMMARY:This is a really long summary to test the method of unfolding lines \, so I'm just going to make it a whole bunch of lines. With a twist: a " ö" takes up multiple bytes\, and should be wrapped to the next line. ATTACH:http://bush.sucks.org/impeach/him.rhtml ATTACH:http://corporations-dominate.existence.net/why.rhtml RDATE;TZID=US-Mountain:20050121T170000,20050122T170000 X-TEST-COMPONENT;QTEST="Hello, World":Shouldn't double double quotes END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/nondefault_values.ics0000644000004100000410000000156614715202173022602 0ustar www-datawww-dataBEGIN:VCALENDAR VERSION:2.0 CALSCALE:GREGORIAN PRODID:iCalendar-Ruby X-WR-CALNAME:Test Long Description BEGIN:VEVENT DESCRIPTION:FULL DETAILS:\nhttp://test.com/events/570\n\nCary Brothers walk s the same musical ground as Pete Yorn\, Nick Drake\, Jeff Buckley and othe rs\; crafting emotional melodies\, with strong vocals and thoughtful lyrics . Brett Dennen has "\;that thing."\; Inspired fans describe it: &qu ot\;lush shimmering vocals\, an intricately groovin'\; guitar style\, a lyrical beauty rare in a young songwriter\,"\; and "\;this soulful blend of everything that feels good."\; Rising up around him is music\; transcending genres\, genders and generations. DTEND;VALUE=DATE:20061215 DTSTAMP:20061215T114034 DTSTART;VALUE=DATE:20061215 SEQUENCE:1001 SUMMARY:DigiWorld 2006 UID:foobar URL:http://test.com/events/644 END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/single_event.ics0000644000004100000410000000147314715202173021543 0ustar www-datawww-dataBEGIN:VCALENDAR VERSION:2.0 PRODID:bsprodidfortestabc123 CALSCALE:GREGORIAN BEGIN:VEVENT DTSTAMP:20050118T211523Z UID:bsuidfortestabc123 DTSTART;TZID=US-Mountain:20050120T170000 DTEND;TZID=US-Mountain:20050120T184500 CLASS:PRIVATE GEO:37.386013;-122.0829322 ORGANIZER;CN="Joe Bob: Magician":mailto:joebob@random.net PRIORITY:2 SUMMARY:This is a really long summary to test the method of unfolding lines \, so I'm just going to make it a whole bunch of lines. With a twist: a " ö" takes up multiple bytes\, and should be wrapped to the next line. ATTACH:http://bush.sucks.org/impeach/him.rhtml ATTACH:http://corporations-dominate.existence.net/why.rhtml RDATE;TZID=US-Mountain:20050121T170000,20050122T170000 X-TEST-COMPONENT;QTEST="Hello, World":Shouldn't double double quotes END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/two_events.ics0000644000004100000410000000164614715202173021260 0ustar www-datawww-dataBEGIN:VEVENT DTSTAMP:20050118T211523Z UID:bsuidfortestabc123 DTSTART;TZID=US-Mountain:20050120T170000 DTEND;TZID=US-Mountain:20050120T184500 CLASS:PRIVATE GEO:37.386013;-122.0829322 ORGANIZER:mailto:joebob@random.net PRIORITY:2 SUMMARY:This is a really long summary to test the method of unfolding lines \, so I'm just going to make it a whole bunch of lines. ATTACH:http://bush.sucks.org/impeach/him.rhtml ATTACH:http://corporations-dominate.existence.net/why.rhtml RDATE;TZID=US-Mountain:20050121T170000,20050122T170000 X-TEST-COMPONENT;QTEST="Hello, World":Shouldn't double double quotes END:VEVENT BEGIN:VEVENT DTSTAMP:20110118T211523Z UID:uid-1234-uid-4321 DTSTART;TZID=US-Mountain:20110120T170000 DTEND;TZID=US-Mountain:20110120T184500 CLASS:PRIVATE GEO:37.386013;-122.0829322 ORGANIZER:mailto:jmera@jmera.human PRIORITY:2 SUMMARY:This is a very short summary. RDATE;TZID=US-Mountain:20110121T170000,20110122T170000 END:VEVENT icalendar-2.10.3/spec/fixtures/single_event_bad_line.ics0000644000004100000410000000132614715202173023355 0ustar www-datawww-dataBEGIN:VCALENDAR VERSION:2.0 PRODID:bsprodidfortestabc123 CALSCALE:GREGORIAN BEGIN:VEVENT DTSTAMP:20050118T211523Z UID:bsuidfortestabc123 DTSTART;TZID=US-Mountain:20050120T170000 DTEND;TZID=US-Mountain:20050120T184500 CLASS:PRIVATE GEO:37.386013;-122.0829322 ORGANIZER:mailto:joebob@random.net PRIORITY:2 SUMMARY:This is a really long summary to test the method of unfolding lines \, so I'm just going to make it a whole bunch of lines. ATTACH:http://bush.sucks.org/impeach/him.rhtml ATTACH:http://corporations-dominate.existence.net/why.rhtml RDATE;TZID=US-Mountain:20050121T170000,20050122T170000 X-TEST-COMPONENT;QTEST="Hello, World":Shouldn't double double quotes X-NO-VALUE END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/two_date_time_events.ics0000644000004100000410000000273514715202173023273 0ustar www-datawww-dataBEGIN:VCALENDAR METHOD:PUBLISH VERSION:2.0 X-WR-CALNAME:TMP PRODID:-//Apple Inc.//Mac OS X 10.9.4//EN X-APPLE-CALENDAR-COLOR:#0E61B9 X-WR-TIMEZONE:America/New_York CALSCALE:GREGORIAN BEGIN:VTIMEZONE TZID:America/New_York BEGIN:DAYLIGHT TZOFFSETFROM:-0500 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU DTSTART:20070311T020000 TZNAME:EDT TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:-0400 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU DTSTART:20071104T020000 TZNAME:EST TZOFFSETTO:-0500 END:STANDARD END:VTIMEZONE BEGIN:VEVENT STATUS:CONFIRMED CREATED:20140731T162556Z UID:7C4A5440-ABA5-4B32-A69D-AFD243B39FE1 DTEND;TZID=America/New_York:20140714T100000 TRANSP:OPAQUE SUMMARY:9am LAST-MODIFIED:20140731T162556Z DTSTAMP:20140731T164042Z DTSTART;TZID=America/New_York:20140714T090000 LOCATION: SEQUENCE:13 BEGIN:VALARM X-WR-ALARMUID:6F54CD66-F2A9-491D-8892-7E3209F6A2E4 UID:6F54CD66-F2A9-491D-8892-7E3209F6A2E4 TRIGGER;VALUE=DATE-TIME:19760401T005545Z ACTION:NONE END:VALARM END:VEVENT BEGIN:VEVENT STATUS:CONFIRMED CREATED:20140731T162556Z UID:A50FA12F-BCA9-4822-A61D-E86EB7440A16 DTEND;VALUE=DATE:20140715 TRANSP:TRANSPARENT SUMMARY:All Day LAST-MODIFIED:20140731T162556Z DTSTAMP:20140731T164829Z DTSTART;VALUE=DATE:20140714 LOCATION: SEQUENCE:18 BEGIN:VALARM X-WR-ALARMUID:295F2CF7-1C48-4E76-A8FC-718A1EBEDB76 UID:295F2CF7-1C48-4E76-A8FC-718A1EBEDB76 TRIGGER;VALUE=DATE-TIME:19760401T005545Z ACTION:NONE END:VALARM END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/two_day_events.ics0000644000004100000410000000141014715202173022102 0ustar www-datawww-dataBEGIN:VCALENDAR METHOD:PUBLISH VERSION:2.0 X-WR-CALNAME:TMP PRODID:-//Apple Inc.//Mac OS X 10.9.4//EN X-APPLE-CALENDAR-COLOR:#0E61B9 X-WR-TIMEZONE:America/New_York CALSCALE:GREGORIAN BEGIN:VEVENT STATUS:CONFIRMED CREATED:20140731T162556Z UID:A50FA12F-BCA9-4822-A61D-E86EB7440A16 DTEND;VALUE=DATE:20140715 TRANSP:TRANSPARENT SUMMARY:Monday LAST-MODIFIED:20140731T162556Z DTSTAMP:20140731T162628Z DTSTART;VALUE=DATE:20140714 LOCATION: SEQUENCE:3 END:VEVENT BEGIN:VEVENT STATUS:CONFIRMED CREATED:20140731T162556Z UID:7C4A5440-ABA5-4B32-A69D-AFD243B39FE1 DTEND;VALUE=DATE:20140714 TRANSP:TRANSPARENT SUMMARY:Sunday LAST-MODIFIED:20140731T162556Z DTSTAMP:20140731T162611Z DTSTART;VALUE=DATE:20140713 LOCATION: SEQUENCE:5 END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/tzid_search.ics0000644000004100000410000000133714715202173021357 0ustar www-datawww-dataBEGIN:VCALENDAR METHOD:PUBLISH VERSION:2.0 CALSCALE:GREGORIAN BEGIN:VTIMEZONE TZID:(GMT-05:00) Eastern Time (US & Canada) BEGIN:STANDARD DTSTART:16011104T010000 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZOFFSETFROM:-0400 TZOFFSETTO:-0500 END:STANDARD BEGIN:DAYLIGHT DTSTART:16010311T030000 RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZOFFSETFROM:-0500 TZOFFSETTO:-0400 END:DAYLIGHT END:VTIMEZONE BEGIN:VEVENT CREATED:20120104T225934Z UID:23D50AF6-23D4-4CB5-85C6-4826F3F19999 DTEND;TZID="(GMT-05:00) Eastern Time (US & Canada)":20180104T130000 RRULE:FREQ=WEEKLY;INTERVAL=1 TRANSP:OPAQUE SUMMARY:Recurring on Wed DTSTART;TZID="(GMT-05:00) Eastern Time (US & Canada)":20180104T100000 DTSTAMP:20120104T231637Z SEQUENCE:6 END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/bad_wrapping.ics0000644000004100000410000000054514715202173021515 0ustar www-datawww-dataBEGIN:VCALENDAR VERSION:2.0 PRODID:manual CALSCALE:GREGORIAN BEGIN:VEVENT DTSTAMP:20200902T223352Z UID:6e7d7fe5-6735-4cdd-bfe4-761dfcecd7a7 DTSTART;VALUE=DATE:20200902 DTEND;VALUE=DATE:20200903 DESCRIPTION:Event description that puts a UTF-8 multi-octet sequence right  here. SUMMARY:UTF-8 multi-octet sequence test END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/single_event_bad_dtstart.ics0000644000004100000410000000126214715202173024112 0ustar www-datawww-dataBEGIN:VCALENDAR VERSION:2.0 PRODID:bsprodidfortestabc123 CALSCALE:GREGORIAN BEGIN:VEVENT DTSTAMP:20050118T211523Z UID:bsuidfortestabc123 DTSTART:20050120 DTEND;TZID=US-Mountain:20050120T184500 CLASS:PRIVATE GEO:37.386013;-122.0829322 ORGANIZER:mailto:joebob@random.net PRIORITY:2 SUMMARY:This is a really long summary to test the method of unfolding lines \, so I'm just going to make it a whole bunch of lines. ATTACH:http://bush.sucks.org/impeach/him.rhtml ATTACH:http://corporations-dominate.existence.net/why.rhtml RDATE;TZID=US-Mountain:20050121T170000,20050122T170000 X-TEST-COMPONENT;QTEST="Hello, World":Shouldn't double double quotes END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/two_time_events.ics0000644000004100000410000000277614715202173022303 0ustar www-datawww-dataBEGIN:VCALENDAR METHOD:PUBLISH VERSION:2.0 X-WR-CALNAME:TMP PRODID:-//Apple Inc.//Mac OS X 10.9.4//EN X-APPLE-CALENDAR-COLOR:#0E61B9 X-WR-TIMEZONE:America/New_York CALSCALE:GREGORIAN BEGIN:VTIMEZONE TZID:America/New_York BEGIN:DAYLIGHT TZOFFSETFROM:-0500 RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU DTSTART:20070311T020000 TZNAME:EDT TZOFFSETTO:-0400 END:DAYLIGHT BEGIN:STANDARD TZOFFSETFROM:-0400 RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU DTSTART:20071104T020000 TZNAME:EST TZOFFSETTO:-0500 END:STANDARD END:VTIMEZONE BEGIN:VEVENT STATUS:CONFIRMED CREATED:20140731T162556Z UID:7C4A5440-ABA5-4B32-A69D-AFD243B39FE1 DTEND;TZID=America/New_York:20140714T100000 TRANSP:OPAQUE SUMMARY:9am LAST-MODIFIED:20140731T162556Z DTSTAMP:20140731T164042Z DTSTART;TZID=America/New_York:20140714T090000 LOCATION: SEQUENCE:13 BEGIN:VALARM X-WR-ALARMUID:6F54CD66-F2A9-491D-8892-7E3209F6A2E4 UID:6F54CD66-F2A9-491D-8892-7E3209F6A2E4 TRIGGER;VALUE=DATE-TIME:19760401T005545Z ACTION:NONE END:VALARM END:VEVENT BEGIN:VEVENT STATUS:CONFIRMED CREATED:20140731T162556Z UID:A50FA12F-BCA9-4822-A61D-E86EB7440A16 DTEND;TZID=America/New_York:20140714T095900 TRANSP:OPAQUE SUMMARY:9:01-9:59 LAST-MODIFIED:20140731T162556Z DTSTAMP:20140731T164405Z DTSTART;TZID=America/New_York:20140714T090100 LOCATION: SEQUENCE:16 BEGIN:VALARM X-WR-ALARMUID:295F2CF7-1C48-4E76-A8FC-718A1EBEDB76 UID:295F2CF7-1C48-4E76-A8FC-718A1EBEDB76 TRIGGER;VALUE=DATE-TIME:19760401T005545Z ACTION:NONE END:VALARM END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/timezone.ics0000644000004100000410000000137414715202173020713 0ustar www-datawww-dataBEGIN:VCALENDAR VERSION:2.0 PRODID:icalendar-ruby CALSCALE:GREGORIAN BEGIN:VTIMEZONE TZID:America/New_York BEGIN:DAYLIGHT DTSTART:20070311T020000 TZOFFSETFROM:-0500 TZOFFSETTO:-0400 RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3 TZNAME:EDT END:DAYLIGHT BEGIN:STANDARD DTSTART:20071104T020000 TZOFFSETFROM:-0400 TZOFFSETTO:-0500 RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11 TZNAME:EST END:STANDARD END:VTIMEZONE BEGIN:VEVENT DTSTAMP:20120320T014112Z UID:9B9CB907-F25F-4D27-B6F3-6DE3A3397BC2 DTSTART;TZID=America/New_York:20120323T190000 DTEND;TZID=America/New_York:20120323T230000 CREATED:20120312T191609Z LAST-MODIFIED:20120320T014105Z LOCATION:Baltimore SEQUENCE:0 STATUS:CONFIRMED SUMMARY:Eastern Event TRANSP:OPAQUE END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/recurrence.ics0000644000004100000410000000074114715202173021213 0ustar www-datawww-dataBEGIN:VCALENDAR VERSION:2.0 PRODID:bsprodidfortestabc123 CALSCALE:GREGORIAN BEGIN:VEVENT DTSTAMP:20050118T211523Z UID:bsuidfortestabc123 DTSTART;VALUE=DATE:20050323 DTEND;VALUE=DATE:20050323 CLASS:PRIVATE ORGANIZER:mailto:joebob@random.net PRIORITY:2 SUMMARY:This is a really long summary to test the method of unfolding lines \, so I'm just going to make it a whole bunch of lines. EXDATE;VALUE=DATE:20120323,20130323 RRULE:FREQ=YEARLY END:VEVENT END:VCALENDAR icalendar-2.10.3/spec/fixtures/event.ics0000644000004100000410000000113414715202173020174 0ustar www-datawww-dataBEGIN:VEVENT DTSTAMP:20050118T211523Z UID:bsuidfortestabc123 DTSTART;TZID=US-Mountain:20050120T170000 DTEND;TZID=US-Mountain:20050120T184500 CLASS:PRIVATE GEO:37.386013;-122.0829322 ORGANIZER:mailto:joebob@random.net PRIORITY:2 SUMMARY:This is a really long summary to test the method of unfolding lines \, so I'm just going to make it a whole bunch of lines. ATTACH:http://bush.sucks.org/impeach/him.rhtml ATTACH:http://corporations-dominate.existence.net/why.rhtml RDATE;TZID=US-Mountain:20050121T170000,20050122T170000 X-TEST-COMPONENT;QTEST="Hello, World":Shouldn't double double quotes END:VEVENT icalendar-2.10.3/spec/values/0000755000004100000410000000000014715202173016002 5ustar www-datawww-dataicalendar-2.10.3/spec/values/text_spec.rb0000644000004100000410000000506514715202173020333 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Values::Text do subject { described_class.new value } let(:unescaped) { "This \\ that, semi; colons\r\nAnother line: \"why not?\"" } let(:escaped) { 'This \\\\ that\, semi\; colons\nAnother line: "why not?"' } describe '#value_ical' do let(:value) { unescaped } it 'escapes \ , ; NL' do expect(subject.value_ical).to eq escaped end end describe 'unescapes in initializer' do context 'given escaped version' do let(:unescaped_no_cr) { unescaped.gsub "\r", '' } let(:value) { escaped } it 'removes escaping' do expect(subject.value).to eq unescaped_no_cr end end context 'given unescaped version' do let(:value) { unescaped } it 'does not try to double unescape' do expect(subject.value).to eq unescaped end end end describe 'escapes parameter text properly' do subject { described_class.new escaped, {'param' => param_value} } context 'single value, no special characters' do let(:param_value) { 'HelloWorld' } it 'does not wrap param in double quotes' do expect(subject.params_ical).to eq %(;PARAM=HelloWorld) end end context 'single value, special characters' do let(:param_value) { 'Hello:World' } it 'wraps param value in double quotes' do expect(subject.params_ical).to eq %(;PARAM="Hello:World") end end context 'single value, double quotes' do let(:param_value) { 'Hello "World"' } it 'replaces double quotes with single' do expect(subject.params_ical).to eq %(;PARAM=Hello 'World') end end context 'multiple values, no special characters' do let(:param_value) { ['HelloWorld', 'GoodbyeMoon'] } it 'joins with comma' do expect(subject.params_ical).to eq %(;PARAM=HelloWorld,GoodbyeMoon) end end context 'multiple values, with special characters' do let(:param_value) { ['Hello, World', 'GoodbyeMoon'] } it 'quotes values with special characters, joins with comma' do expect(subject.params_ical).to eq %(;PARAM="Hello, World",GoodbyeMoon) end end context 'multiple values, double quotes' do let(:param_value) { ['Hello, "World"', 'GoodbyeMoon'] } it 'replaces double quotes with single' do expect(subject.params_ical).to eq %(;PARAM="Hello, 'World'",GoodbyeMoon) end end context 'nil value' do let(:param_value) { nil } it 'trats nil as blank' do expect(subject.params_ical).to eq %(;PARAM=) end end end end icalendar-2.10.3/spec/values/utc_offset_spec.rb0000644000004100000410000000165314715202173021507 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Values::UtcOffset do subject { described_class.new value } describe '#value_ical' do let(:value) { '-050000' } it 'does not output seconds unless required' do expect(subject.value_ical).to eq '-0500' end context 'with seconds' do let(:value) { '+023030' } specify { expect(subject.value_ical).to eq '+023030' } end end describe '#behind?' do context 'negative offset' do let(:value) { '-0500' } specify { expect(subject.behind?).to be true } end context 'positive offset' do let(:value) { '+0200' } specify { expect(subject.behind?).to be false } end context 'no offset' do let(:value) { '-0000' } specify { expect(subject.behind?).to be false } it 'does not allow override' do subject.behind = true expect(subject.behind?).to be false end end end end icalendar-2.10.3/spec/values/date_spec.rb0000644000004100000410000000102114715202173020250 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Values::Date do subject { described_class.new value, params } let(:value) { '20140209' } let(:params) { {} } describe "#==" do let(:other) { described_class.new value } it "is equivalent to another Icalendar::Values::Date" do expect(subject).to eq other end context "differing params" do let(:params) { {"x-custom-param": "param-value"} } it "is no longer equivalent" do expect(subject).not_to eq other end end end end icalendar-2.10.3/spec/values/date_or_date_time_spec.rb0000644000004100000410000000255114715202173022774 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Values::DateOrDateTime do subject { described_class.new value, params } let(:params) { {} } describe '#call' do context 'DateTime value' do let(:value) { '20140209T194355Z' } it 'returns a DateTime object' do expect(subject.call).to be_a_kind_of(Icalendar::Values::DateTime) end it 'has the proper value' do expect(subject.call.value).to eq DateTime.new(2014, 2, 9, 19, 43, 55) end end context 'Date value' do let(:value) { '20140209' } it 'returns a Date object' do expect(subject.call).to be_a_kind_of(Icalendar::Values::Date) end it 'has the proper value' do expect(subject.call.value).to eq Date.new(2014, 2, 9) end end context 'unparseable date' do let(:value) { '99999999' } it 'raises an error including the unparseable time' do expect { subject.call }.to raise_error(ArgumentError, %r{Failed to parse \"#{value}\"}) end end end describe "#to_ical" do let(:event) { Icalendar::Event.new } let(:time_stamp) { Time.now.strftime Icalendar::Values::DateTime::FORMAT } it "should call parse behind the scenes" do event.dtstart = described_class.new time_stamp, "tzid" => "UTC" expect(event.to_ical).to include "DTSTART:#{time_stamp}Z" end end end icalendar-2.10.3/spec/values/period_spec.rb0000644000004100000410000000262714715202173020632 0ustar www-datawww-datarequire 'spec_helper' require 'ostruct' describe Icalendar::Values::Period do subject { described_class.new value } context 'date-time/date-time' do let(:value) { '19830507T000600Z/20140128T201400Z' } describe '#value_ical' do specify { expect(subject.value_ical).to eq value } end describe '#period_start' do specify { expect(subject.period_start).to eq DateTime.new(1983, 5, 7, 0, 6) } end describe '#duration' do specify { expect(subject.duration).to be_nil } end describe '#explicit_end' do specify { expect(subject.explicit_end).to eq DateTime.new(2014, 01, 28, 20, 14) } end end context 'date-time/duration' do let(:value) { '19830507T000600Z/P1604W' } let(:expected_duration) { OpenStruct.new past: false, weeks: 1604, days: 0, hours: 0, minutes: 0, seconds: 0 } describe '#value_ical' do specify { expect(subject.value_ical).to eq value } it 'allows updating duration' do subject.duration = 'PT30M' expect(subject.value_ical).to eq '19830507T000600Z/PT30M' end end describe '#period_start' do specify { expect(subject.period_start).to eq DateTime.new(1983, 5, 7, 0, 6) } end describe '#duration' do specify { expect(subject.duration).to eq expected_duration } end describe '#explicit_end' do specify { expect(subject.explicit_end).to eq nil } end end endicalendar-2.10.3/spec/values/date_time_spec.rb0000644000004100000410000000643514715202173021304 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Values::DateTime do subject { described_class.new value, params } let(:value) { '20140209T194355Z' } let(:params) { {} } # not sure how to automatically test both sides of this. # For now - relying on commenting out dev dependency in gemspec # AND uninstalling gem manually if defined? ActiveSupport context 'with ActiveSupport' do it 'parses a string to a ActiveSupport::TimeWithZone instance' do expect(subject.value).to be_a_kind_of ActiveSupport::TimeWithZone expect(subject.value_ical).to eq value end context 'local time' do let(:value) { '20140209T160652' } let(:params) { {'tzid' => 'America/Denver'} } it 'parses the value as local time' do expect(subject.value.hour).to eq 16 expect(subject.value.utc_offset).to eq -25200 expect(subject.value.utc.hour).to eq 23 end end context 'nonstandard format tzid local time' do let(:value) { '20230901T230404' } let(:params) { {'tzid' => 'Singapore Standard Time'} } it 'parses the value as local time' do expect(subject.value.hour).to eq 23 expect(subject.value.utc_offset).to eq 28800 expect(subject.value.utc.hour).to eq 15 end it 'updates the tzid' do # use an array because that's how output from the parser will end up expect(subject.ical_params['tzid']).to eq ['Asia/Singapore'] end end end else context 'without ActiveSupport' do it 'parses a string to a DateTime instance' do expect(subject.value).to be_a_kind_of ::DateTime end context 'local time' do let(:value) { '20140209T160652' } let(:params) { {'tzid' => 'America/Denver'} } it 'parses the value as local time' do expect(subject.value.hour).to eq 16 # TODO better offset support without ActiveSupport #expect(subject.offset).to eq Rational(-7, 24) end end end end context 'common tests' do it 'does not add any tzid parameter to output' do expect(subject.to_ical described_class).to eq ":#{value}" end context 'manually set UTC' do let(:value) { '20140209T194355' } let(:params) { {'TZID' => 'UTC'} } it 'does not add a tzid parameter, but does add a Z' do expect(subject.to_ical described_class).to eq ":#{value}Z" end end context 'local time' do let(:value) { '20140209T160652' } let(:params) { {'tzid' => 'America/Denver'} } it 'keeps value and tzid as localtime on output' do expect(subject.to_ical described_class).to eq ";TZID=America/Denver:#{value}" end end context 'floating local time' do let(:value) { '20140209T162845' } it 'keeps the value as a DateTime' do expect(subject.value).to be_a_kind_of ::DateTime end it 'does not append a Z on output' do expect(subject.to_ical described_class).to eq ":#{value}" end end context 'unparseable time' do let(:value) { 'unparseable_time' } it 'raises an error including the unparseable time' do expect { subject }.to raise_error(ArgumentError, %r{Failed to parse \"#{value}\"}) end end end end icalendar-2.10.3/spec/values/recur_spec.rb0000644000004100000410000000313414715202173020462 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Values::Recur do subject { described_class.new value } let(:value) { 'FREQ=DAILY' } describe 'parsing' do context 'multiple bydays' do let(:value) { 'FREQ=WEEKLY;COUNT=4;BYDAY=MO,WE,FR' } specify { expect(subject.frequency).to eq 'WEEKLY' } specify { expect(subject.count).to eq 4 } specify { expect(subject.by_day).to eq %w(MO WE FR) } end context 'single byday' do let(:value) { 'FREQ=YEARLY;BYDAY=2SU;BYMONTH=3' } specify { expect(subject.frequency).to eq 'YEARLY' } specify { expect(subject.by_day).to eq %w(2SU) } specify { expect(subject.by_month).to eq [3] } end context 'neverending yearly' do let(:value) { 'FREQ=YEARLY' } specify { expect(subject.frequency).to eq 'YEARLY' } it 'can be added to another event by sending' do event = Icalendar::Event.new event.send "rrule=", subject rule = event.send "rrule" expect(rule.first.frequency).to eq 'YEARLY' end end end describe '#valid?' do it 'requires frequency' do expect(subject.valid?).to be true subject.frequency = nil expect(subject.valid?).to be false end it 'cannot have both until and count' do subject.until = '20140201' subject.count = 4 expect(subject.valid?).to be false end end describe '#value_ical' do let(:value) { 'FREQ=DAILY;BYYEARDAY=1,34,56,240;BYDAY=SU,SA' } it 'outputs in spec order' do expect(subject.value_ical).to eq 'FREQ=DAILY;BYDAY=SU,SA;BYYEARDAY=1,34,56,240' end end end icalendar-2.10.3/spec/values/duration_spec.rb0000644000004100000410000000271414715202173021172 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Values::Duration do subject { described_class.new value } describe '#past?' do context 'positive explicit' do let(:value) { '+P15M' } specify { expect(subject.past?).to be false } end context 'positive implicit' do let(:value) { 'P15M' } specify { expect(subject.past?).to be false } end context 'negative' do let(:value) { '-P15M' } specify { expect(subject.past?).to be true } end end describe '#days' do context 'days given' do let(:value) { 'P5DT3H' } specify { expect(subject.days).to eq 5 } end context 'no days given' do let(:value) { 'P5W' } specify { expect(subject.days).to eq 0 } end end describe '#weeks' do let(:value) { 'P3W' } specify { expect(subject.weeks).to eq 3 } end describe '#hours' do let(:value) { 'PT6H' } specify { expect(subject.hours).to eq 6 } end describe '#minutes' do let(:value) { 'P2DT5H45M12S' } specify { expect(subject.minutes).to eq 45 } end describe '#seconds' do let(:value) { '-PT5M30S' } specify { expect(subject.seconds).to eq 30 } end describe '#value_ical' do let(:value) { 'P2DT4H' } specify { expect(subject.value_ical).to eq value } end describe '#days=' do let(:value) { 'P3D' } it 'can set the number of days' do subject.days = 4 expect(subject.value_ical).to eq 'P4D' end end end icalendar-2.10.3/spec/alarm_spec.rb0000644000004100000410000000641014715202173017137 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Alarm do # currently no behavior in Alarm not tested other places describe '#valid?' do subject do described_class.new.tap do |a| a.trigger = Icalendar::Values::DateTime.new(Time.now.utc) end end context 'neither duration or repeat is set' do before(:each) { subject.action = 'AUDIO' } it { should be_valid } end context 'both duration and repeat are set' do before(:each) { subject.action = 'AUDIO' } before(:each) do subject.duration = 'PT15M' subject.repeat = 4 end it { should be_valid } end context 'only duration is set' do before(:each) { subject.duration = 'PT15M' } it { should_not be_valid } end context 'only repeat is set' do before(:each) { subject.repeat = 4 } it { should_not be_valid } end context 'DISPLAY is set as default action' do it 'must be DISPLAY' do expect(subject.action).to eq 'DISPLAY' end end context 'display action' do it 'requires description' do expect(subject).to_not be_valid subject.description = 'Display Text' expect(subject).to be_valid end end context 'email action' do before(:each) { subject.action = 'EMAIL' } context 'requires subject and body' do before(:each) { subject.attendee = ['mailto:test@email.com'] } it 'requires description' do subject.summary = 'Email subject' expect(subject).to_not be_valid subject.description = 'Email Body' expect(subject).to be_valid end it 'requires summary' do subject.description = 'Email body' expect(subject).to_not be_valid subject.summary = 'Email subject' expect(subject).to be_valid end end context 'attendees are required' do before(:each) do subject.summary = 'subject' subject.description = 'body' end it 'must be present' do subject.attendee = nil expect(subject).to_not be_valid end it 'can be single' do subject.attendee << 'mailto:test@email.com' expect(subject).to be_valid end it 'can be multi' do subject.attendee << 'mailto:test@email.com' subject.attendee << 'mailto:email@test.com' expect(subject).to be_valid end end end context 'strict validations check parent' do subject do described_class.new.tap do |a| a.action = 'AUDIO' a.trigger = Icalendar::Values::DateTime.new(Time.now.utc) end end specify { expect(subject.valid? true).to be true } context 'with parent' do before(:each) { subject.parent = parent } context 'event' do let(:parent) { Icalendar::Event.new } specify { expect(subject.valid? true).to be true } end context 'todo' do let(:parent) { Icalendar::Todo.new } specify { expect(subject.valid? true).to be true } end context 'journal' do let(:parent) { Icalendar::Journal.new } specify { expect(subject.valid? true).to be false } end end end end end icalendar-2.10.3/spec/parser_spec.rb0000644000004100000410000000710314715202173017337 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Parser do subject { described_class.new source, false } let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', fn) } describe '#parse' do context 'reversed_timezone.ics' do let(:fn) { 'reversed_timezone.ics' } it 'correctly parses the event timezone' do event = subject.parse.first.events.first expect(event.dtstart.utc_offset).to eq -25200 end end context 'single_event.ics' do let(:fn) { 'single_event.ics' } it 'returns an array of calendars' do parsed = subject.parse expect(parsed).to be_instance_of Array expect(parsed.count).to eq 1 expect(parsed[0]).to be_instance_of Icalendar::Calendar end it 'properly splits multi-valued lines' do event = subject.parse.first.events.first expect(event.geo).to eq [37.386013,-122.0829322] end it 'saves params' do event = subject.parse.first.events.first expect(event.dtstart.ical_params).to eq('tzid' => ['US-Mountain']) end end context 'recurrence.ics' do let(:fn) { 'recurrence.ics' } it 'correctly parses the exdate array' do event = subject.parse.first.events.first ics = event.to_ical expect(ics).to match 'EXDATE;VALUE=DATE:20120323,20130323' end end context 'event.ics' do let(:fn) { 'event.ics' } before { subject.component_class = Icalendar::Event } it 'returns an array of events' do parsed = subject.parse expect(parsed).to be_instance_of Array expect(parsed.count).to be 1 expect(parsed[0]).to be_instance_of Icalendar::Event end end context 'events.ics' do let(:fn) { 'two_events.ics' } before { subject.component_class = Icalendar::Event } it 'returns an array of events' do events = subject.parse expect(events.count).to be 2 expect(events.first.uid).to eq("bsuidfortestabc123") expect(events.last.uid).to eq("uid-1234-uid-4321") end end context 'tzid_search.ics' do let(:fn) { 'tzid_search.ics' } it 'correctly sets the weird tzid' do parsed = subject.parse event = parsed.first.events.first expect(event.dtstart.utc).to eq Time.parse("20180104T150000Z") end end context 'custom_component.ics' do let(:fn) { 'custom_component.ics' } it 'correctly handles custom named components' do parsed = subject.parse calendar = parsed.first expect(calendar.custom_component('x_event_series').size).to eq 1 expect(calendar.custom_component('X-EVENT-SERIES').size).to eq 1 end end end describe '#parse with bad line' do let(:fn) { 'single_event_bad_line.ics' } it 'returns an array of calendars' do parsed = subject.parse expect(parsed).to be_instance_of Array expect(parsed.count).to eq 1 expect(parsed[0]).to be_instance_of Icalendar::Calendar end it 'properly splits multi-valued lines' do event = subject.parse.first.events.first expect(event.geo).to eq [37.386013,-122.0829322] end it 'saves params' do event = subject.parse.first.events.first expect(event.dtstart.ical_params).to eq('tzid' => ['US-Mountain']) end end describe 'missing date value parameter' do let(:fn) { 'single_event_bad_dtstart.ics' } it 'falls back to date type for dtstart' do event = subject.parse.first.events.first expect(event.dtstart).to be_kind_of Icalendar::Values::Date end end end icalendar-2.10.3/spec/freebusy_spec.rb0000644000004100000410000000017114715202173017665 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Freebusy do # currently no behavior in Journal not tested other places endicalendar-2.10.3/spec/roundtrip_spec.rb0000644000004100000410000001352214715202173020073 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar do describe 'single event round trip' do let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'single_event.ics') } it 'will generate the same file as is parsed' do ical = Icalendar::Calendar.parse(source).first.to_ical expect(ical).to eq source end it 'array properties can be assigned to a new event' do event = Icalendar::Event.new parsed = Icalendar::Calendar.parse(source).first event.rdate = parsed.events.first.rdate expect(event.rdate.first).to be_kind_of Icalendar::Values::Helpers::Array expect(event.rdate.first.params_ical).to eq ";TZID=US-Mountain" end end describe 'cleanly handle facebook organizers' do let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'single_event_bad_organizer.ics') } let(:source_lowered_uri) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'single_event_organizer_parsed.ics') } it 'will generate the same file as it parsed' do ical = Icalendar::Calendar.parse(source).first.to_ical source_equal = ical == source # rbx-3 parses the organizer as a URI, which strips the space and lowercases everything after the first : # this is correct behavior, according to the icalendar spec, so we're not fudging the parser to accomodate # facebook not properly wrapping the CN param in dquotes source_lowered_equal = ical == source_lowered_uri expect(source_equal || source_lowered_equal).to be true end end describe 'timezone round trip' do let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'timezone.ics') } it 'will generate the same file as it parsed' do ical = Icalendar::Calendar.parse(source).first.to_ical expect(ical).to eq source end end describe 'non-default values' do let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'nondefault_values.ics') } subject { Icalendar::Calendar.parse(source).first.events.first } it 'will set dtstart to Date' do expect(subject.dtstart.value).to eq ::Date.new(2006, 12, 15) end it 'will set dtend to Date' do expect(subject.dtend.value).to eq ::Date.new(2006, 12, 15) end it 'will output value param on dtstart' do expect(subject.dtstart.to_ical(subject.class.default_property_types['dtstart'])).to match /^;VALUE=DATE:20061215$/ end it 'will output value param on dtend' do expect(subject.dtend.to_ical(subject.class.default_property_types['dtend'])).to match /^;VALUE=DATE:20061215$/ end end describe 'sorting daily events' do let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'two_day_events.ics') } subject { Icalendar::Calendar.parse(source).first.events } it 'sorts day events' do events = subject.sort_by(&:dtstart) expect(events.first.dtstart).to eq ::Date.new(2014, 7, 13) expect(events.last.dtstart).to eq ::Date.new(2014, 7, 14) end end describe 'sorting time events' do let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'two_time_events.ics') } subject { Icalendar::Calendar.parse(source).first.events } it 'sorts time events by start time' do events = subject.sort_by(&:dtstart) expect(events.first.dtstart.to_datetime).to eq ::DateTime.new(2014, 7, 14, 9, 0, 0, '-4') expect(events.last.dtstart.to_datetime).to eq ::DateTime.new(2014, 7, 14, 9, 1, 0, '-4') expect(events.last.dtend.to_datetime).to eq ::DateTime.new(2014, 7, 14, 9, 59, 0, '-4') end it 'sorts time events by end time' do events = subject.sort_by(&:dtend) expect(events.first.dtstart.to_datetime).to eq ::DateTime.new(2014, 7, 14, 9, 1, 0, '-4') expect(events.first.dtend.to_datetime).to eq ::DateTime.new(2014, 7, 14, 9, 59, 0, '-4') expect(events.last.dtstart.to_datetime).to eq ::DateTime.new(2014, 7, 14, 9, 0, 0, '-4') end end describe 'sorting date / time events' do let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'two_date_time_events.ics') } subject { Icalendar::Calendar.parse(source).first.events } it 'sorts time events' do events = subject.sort_by(&:dtstart) expect(events.first.dtstart.to_date).to eq ::Date.new(2014, 7, 14) expect(events.last.dtstart.to_datetime).to eq ::DateTime.new(2014, 7, 14, 9, 0, 0, '-4') end end describe 'non-standard values' do if defined? File::NULL before(:all) { Icalendar.logger = Icalendar::Logger.new File::NULL } after(:all) { Icalendar.logger = nil } end let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'nonstandard.ics') } subject { Icalendar::Parser.new(source, strict) } context 'strict parser' do let(:strict) { true } specify { expect { subject.parse }.to raise_error(NoMethodError) } end context 'lenient parser' do let(:strict) { false } specify { expect { subject.parse }.to_not raise_error } context 'saves non-standard fields' do let(:parsed) { subject.parse.first.events.first } specify { expect(parsed.custom_property('customfield').first).to eq 'Not properly noted as custom with X- prefix.' } specify { expect(parsed.custom_property('CUSTOMFIELD').first).to eq 'Not properly noted as custom with X- prefix.' } end it 'can output custom fields' do ical = subject.parse.first.to_ical expect(ical).to include 'CUSTOMFIELD:Not properly noted as custom with X- prefix.' end context 'custom components' do let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', 'custom_component.ics') } it 'can output the custom component' do ical = subject.parse.first.to_ical expect(ical).to include 'BEGIN:X-EVENT-SERIES' end end end end end icalendar-2.10.3/spec/event_spec.rb0000644000004100000410000001224614715202173017170 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Event do describe '#dtstart' do context 'no parent' do it 'is invalid if not set' do expect(subject).to_not be_valid end it 'is valid if set' do subject.dtstart = DateTime.now expect(subject).to be_valid end end context 'with parent' do before(:each) { subject.parent = Icalendar::Calendar.new } it 'is invalid without method set' do expect(subject).to_not be_valid end it 'is valid with parent method set' do subject.parent.ip_method = 'UPDATE' expect(subject).to be_valid end end end context 'mutually exclusive values' do before(:each) { subject.dtstart = DateTime.now } it 'is invalid if both dtend and duration are set' do subject.dtend = Date.today + 1; subject.duration = 'PT15M' expect(subject).to_not be_valid end it 'is valid if dtend is set' do subject.dtend = Date.today + 1; expect(subject).to be_valid end it 'is valid if duration is set' do subject.duration = 'PT15M' expect(subject).to be_valid end end context 'suggested single values' do before(:each) do subject.dtstart = DateTime.now subject.append_rrule 'RRule' subject.append_rrule 'RRule' end it 'is valid by default' do expect(subject).to be_valid end it 'is invalid with strict checking' do expect(subject.valid?(true)).to be false end end context 'multi values' do describe '#comment' do it 'will return an array when set singly' do subject.comment = 'a comment' expect(subject.comment).to eq ['a comment'] end it 'can be appended' do subject.comment << 'a comment' subject.comment << 'b comment' expect(subject.comment).to eq ['a comment', 'b comment'] end it 'can be added' do subject.append_comment 'a comment' expect(subject.comment).to eq ['a comment'] end end if defined? ActiveSupport describe '#rdate' do it 'does not convert a DateTime delegating for an ActiveSupport::TimeWithZone into an Array' do timestamp = '20140130T230000Z' expected = [Icalendar::Values::DateTime.new(timestamp)] subject.rdate = timestamp expect(subject.rdate).to eq(expected) end end end end describe "#append_custom_property" do context "with custom property" do it "appends to the custom properties hash" do subject.append_custom_property "x_my_property", "test value" expect(subject.custom_properties).to eq({"x_my_property" => ["test value"]}) end end context "with a defined property" do it "sets the proper setter" do subject.append_custom_property "summary", "event" expect(subject.summary).to eq "event" expect(subject.custom_properties).to eq({}) end end end describe "#custom_property" do it "returns a default for missing properties" do expect(subject.x_doesnt_exist).to eq([]) expect(subject.custom_property "x_doesnt_exist").to eq([]) end end describe '.parse' do let(:source) { File.read File.join(File.dirname(__FILE__), 'fixtures', fn) } let(:fn) { 'event.ics' } it 'should return an events array' do events = Icalendar::Event.parse(source) expect(events).to be_instance_of Array expect(events.count).to be 1 expect(events.first).to be_instance_of Icalendar::Event end end describe '#find_alarm' do it 'should not respond_to find_alarm' do expect(subject.respond_to?(:find_alarm)).to be false end end describe '#has_alarm?' do context 'without a set valarm' do it { is_expected.not_to have_alarm } end context 'with a set valarm' do before { subject.alarm } it { is_expected.to have_alarm } end end describe '#to_ical' do before(:each) do subject.dtstart = "20131227T013000Z" subject.dtend = "20131227T033000Z" subject.summary = 'My event, my ical, my test' subject.geo = [41.230896,-74.411774] subject.url = 'https://example.com' subject.x_custom_property = 'customize' end it { expect(subject.to_ical).to include 'DTSTART:20131227T013000Z' } it { expect(subject.to_ical).to include 'DTEND:20131227T033000Z' } it { expect(subject.to_ical).to include 'SUMMARY:My event\, my ical\, my test' } it { expect(subject.to_ical).to include 'X-CUSTOM-PROPERTY:customize' } it { expect(subject.to_ical).to include 'URL;VALUE=URI:https://example.com' } it { expect(subject.to_ical).to include 'GEO:41.230896;-74.411774' } context 'simple organizer' do before :each do subject.organizer = 'mailto:jsmith@example.com' end it { expect(subject.to_ical).to include 'ORGANIZER:mailto:jsmith@example.com' } end context 'complex organizer' do before :each do subject.organizer = Icalendar::Values::CalAddress.new("mailto:jsmith@example.com", cn: 'John Smith') end it { expect(subject.to_ical).to include 'ORGANIZER;CN=John Smith:mailto:jsmith@example.com' } end end end icalendar-2.10.3/spec/timezone_spec.rb0000644000004100000410000000567114715202173017705 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Timezone do describe "valid?" do subject { described_class.new.tap { |t| t.tzid = 'Eastern' } } context 'with both standard and daylight components' do before(:each) do subject.daylight { |d| allow(d).to receive(:valid?).and_return true } subject.standard { |s| allow(s).to receive(:valid?).and_return true } end it { should be_valid } end context 'with only standard' do before(:each) { subject.standard { |s| allow(s).to receive(:valid?).and_return true } } it { expect(subject).to be_valid } end context 'with only daylight' do before(:each) { subject.daylight { |d| allow(d).to receive(:valid?).and_return true } } it { expect(subject).to be_valid } end context 'with neither standard or daylight' do it { should_not be_valid } end end context 'marshalling' do context 'with standard/daylight components' do before do subject.standard do |standard| standard.rrule = Icalendar::Values::Recur.new("FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10") standard.dtstart = Icalendar::Values::DateTime.new("16010101T030000") standard.tzoffsetfrom = Icalendar::Values::UtcOffset.new("+0200") standard.tzoffsetto = Icalendar::Values::UtcOffset.new("+0100") end subject.daylight do |daylight| daylight.rrule = Icalendar::Values::Recur.new("FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=3") daylight.dtstart = Icalendar::Values::DateTime.new("16010101T020000") daylight.tzoffsetfrom = Icalendar::Values::UtcOffset.new("+0100") daylight.tzoffsetto = Icalendar::Values::UtcOffset.new("+0200") end end it 'can be de/serialized' do first_standard = subject.standards.first first_daylight = subject.daylights.first expect(first_standard.valid?).to be_truthy expect(first_daylight.valid?).to be_truthy # calling previous_occurrence intializes @cached_occurrences with a time that's not handled by ruby marshaller first_occurence_for = Time.new(1601, 10, 31) standard_previous_occurrence = first_standard.previous_occurrence(first_occurence_for) expect(standard_previous_occurrence).not_to be_nil daylight_previous_occurrence = first_daylight.previous_occurrence(first_occurence_for) expect(daylight_previous_occurrence).not_to be_nil deserialized = nil expect { deserialized = Marshal.load(Marshal.dump(subject)) }.not_to raise_exception expect(deserialized.standards.first.previous_occurrence(first_occurence_for)).to eq(standard_previous_occurrence) expect(deserialized.daylights.first.previous_occurrence(first_occurence_for)).to eq(daylight_previous_occurrence) end end it 'can be de/serialized' do expect { Marshal.load(Marshal.dump(subject)) }.not_to raise_exception end end end icalendar-2.10.3/spec/todo_spec.rb0000644000004100000410000000075414715202173017015 0ustar www-datawww-datarequire 'spec_helper' describe Icalendar::Todo do describe '#dtstart' do it 'is not normally required' do subject.dtstart = nil expect(subject).to be_valid end context 'with duration set' do before(:each) { subject.duration = 'PT15M' } it 'is invalid if not set' do expect(subject).to_not be_valid end it 'is valid when set' do subject.dtstart = Date.today expect(subject).to be_valid end end end end icalendar-2.10.3/.rspec0000644000004100000410000000003214715202173014661 0ustar www-datawww-data--color --format progress icalendar-2.10.3/Rakefile0000644000004100000410000000054014715202173015215 0ustar www-datawww-datarequire 'bundler' Bundler::GemHelper.install_tasks require 'rspec/core/rake_task' RSpec::Core::RakeTask.new task default: [:spec, :build] desc "Load iCalendar in IRB" task :console do require 'irb' require 'irb/completion' $:.unshift File.join(File.dirname(__FILE__), 'lib') require 'icalendar' ARGV.clear IRB.start end icalendar-2.10.3/icalendar.gemspec0000644000004100000410000000376114715202173017047 0ustar www-datawww-datarequire File.join File.dirname(__FILE__), 'lib', 'icalendar', 'version' Gem::Specification.new do |s| s.authors = ['Ryan Ahearn'] s.email = ['ryan.c.ahearn@gmail.com'] s.name = "icalendar" s.version = Icalendar::VERSION s.licenses = ['BSD-2-Clause', 'GPL-3.0-only', 'icalendar'] s.homepage = "https://github.com/icalendar/icalendar" s.platform = Gem::Platform::RUBY s.summary = "A ruby implementation of the iCalendar specification (RFC-5545)." s.description = <<-EOD Implements the iCalendar specification (RFC-5545) in Ruby. This allows for the generation and parsing of .ics files, which are used by a variety of calendaring applications. EOD s.post_install_message = <<-EOM ActiveSupport is required for TimeWithZone support, but not required for general use. EOM s.metadata = { 'changelog_uri' => 'https://github.com/icalendar/icalendar/blob/main/CHANGELOG.md' } s.files = `git ls-files`.split "\n" s.test_files = `git ls-files -- {test,spec,features}/*`.split "\n" s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename f } s.require_paths = ['lib'] s.required_ruby_version = '>= 2.4.0' s.add_dependency 'ice_cube', '~> 0.16' s.add_dependency 'ostruct' s.add_development_dependency 'rake', '~> 13.0' s.add_development_dependency 'bundler', '~> 2.0' # test with all groups of tzinfo dependencies # tzinfo 2.x s.add_development_dependency 'activesupport', '~> 7.1' s.add_development_dependency 'tzinfo', '~> 2.0' s.add_development_dependency 'tzinfo-data', '~> 1.2020' # tzinfo 1.x # s.add_development_dependency 'activesupport', '~> 6.0' # s.add_development_dependency 'tzinfo', '~> 1.2' # s.add_development_dependency 'tzinfo-data', '~> 1.2020' # tzinfo 0.x # s.add_development_dependency 'tzinfo', '~> 0.3' # end tzinfo s.add_development_dependency 'timecop', '~> 0.9' s.add_development_dependency 'rspec', '~> 3.8' s.add_development_dependency 'simplecov', '~> 0.16' s.add_development_dependency 'byebug' end icalendar-2.10.3/Gemfile0000644000004100000410000000004714715202173015045 0ustar www-datawww-datasource 'https://rubygems.org' gemspec icalendar-2.10.3/LICENSE0000644000004100000410000000523714715202173014565 0ustar www-datawww-dataRuby is copyrighted free software by Yukihiro Matsumoto . As of Ruby 1.9.3, the Ruby Language went from a Dual GPL/Ruby license to Dual BSD/Ruby license. The intent of the icalendar license is that it is provided under the same terms as Ruby itself. The way we're going to interpret this is the software can be redistributed under any of the 3 licenses, GPL, BSD, or the conditions below, at your option: 1. You may make and give away verbatim copies of the source form of the software without restriction, provided that you duplicate all of the original copyright notices and associated disclaimers. 2. You may modify your copy of the software in any way, provided that you do at least ONE of the following: a) place your modifications in the Public Domain or otherwise make them Freely Available, such as by posting said modifications to Usenet or an equivalent medium, or by allowing the author to include your modifications in the software. b) use the modified software only within your corporation or organization. c) give non-standard binaries non-standard names, with instructions on where to get the original software distribution. d) make other distribution arrangements with the author. 3. You may distribute the software in object code or binary form, provided that you do at least ONE of the following: a) distribute the binaries and library files of the software, together with instructions (in the manual page or equivalent) on where to get the original distribution. b) accompany the distribution with the machine-readable source of the software. c) give non-standard binaries non-standard names, with instructions on where to get the original software distribution. d) make other distribution arrangements with the author. 4. You may modify and include the part of the software into any other software (possibly commercial). But some files in the distribution are not written by the author, so that they are not under these terms. For the list of those files and their copying conditions, see the file LEGAL. 5. The scripts and library files supplied as input to or produced as output from the software do not automatically fall under the copyright of the software, but belong to whomever generated them, and may be sold commercially, and may be aggregated with this software. 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. icalendar-2.10.3/README.md0000644000004100000410000002547614715202173015046 0ustar www-datawww-dataiCalendar -- Internet calendaring, Ruby style === [![Ruby](https://github.com/icalendar/icalendar/actions/workflows/main.yml/badge.svg)](https://github.com/icalendar/icalendar/actions/workflows/main.yml) [![Code Climate](https://codeclimate.com/github/icalendar/icalendar.png)](https://codeclimate.com/github/icalendar/icalendar) ### Upgrade from 1.x ### 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) DESCRIPTION --- iCalendar is a Ruby library for dealing with iCalendar files in the iCalendar format defined by [RFC-5545](http://tools.ietf.org/html/rfc5545). EXAMPLES --- ### Creating calendars and events ### ```ruby require 'icalendar' # Create a calendar with an event (standard method) cal = Icalendar::Calendar.new cal.event do |e| e.dtstart = Icalendar::Values::Date.new('20050428') e.dtend = Icalendar::Values::Date.new('20050429') e.summary = "Meeting with the man." e.description = "Have a long lunch meeting and decide nothing..." e.ip_class = "PRIVATE" end cal.publish ``` #### Or you can make events like this #### ```ruby event = Icalendar::Event.new event.dtstart = DateTime.civil(2006, 6, 23, 8, 30) event.summary = "A great event!" cal.add_event(event) event2 = cal.event # This automatically adds the event to the calendar event2.dtstart = DateTime.civil(2006, 6, 24, 8, 30) event2.summary = "Another great event!" ``` #### Support for property parameters #### ```ruby params = {"altrep" => "http://my.language.net", "language" => "SPANISH"} event = cal.event do |e| e.dtstart = Icalendar::Values::Date.new('20050428') e.dtend = Icalendar::Values::Date.new('20050429') e.summary = Icalendar::Values::Text.new "This is a summary with params.", params end event.summary.ical_params #=> {'altrep' => 'http://my.language.net', 'language' => 'SPANISH'} # or event = cal.event do |e| e.dtstart = Icalendar::Values::Date.new('20050428') e.dtend = Icalendar::Values::Date.new('20050429') e.summary = "This is a summary with params." e.summary.ical_params = params end event.summary.ical_params #=> {'altrep' => 'http://my.language.net', 'language' => 'SPANISH'} ``` #### Support for Dates or DateTimes 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. ```ruby event = cal.event do |e| e.dtstart = Icalendar::Values::DateOrDateTime.new('20140924') e.dtend = Icalendar::Values::DateOrDateTime.new('20140925').call e.summary = 'This is an all-day event, because DateOrDateTime will return Dates' end ``` #### Support for URLs For clients that can parse and display a URL associated with an event, it's possible to assign one. ```ruby event = cal.event do |e| e.url = 'https://example.com' end ``` #### We can output the calendar as a string #### cal_string = cal.to_ical puts cal_string ALARMS --- ### Within an event ### ```ruby cal.event do |e| # ...other event properties e.alarm do |a| a.action = "EMAIL" a.description = "This is an event reminder" # email body (required) a.summary = "Alarm notification" # email subject (required) a.attendee = %w(mailto:me@my-domain.com mailto:me-too@my-domain.com) # one or more email recipients (required) a.append_attendee "mailto:me-three@my-domain.com" a.trigger = "-PT15M" # 15 minutes before a.append_attach Icalendar::Values::Uri.new("ftp://host.com/novo-procs/felizano.exe", "fmttype" => "application/binary") # email attachments (optional) end e.alarm do |a| a.action = "DISPLAY" # This line isn't necessary, it's the default a.summary = "Alarm notification" a.trigger = "-P1DT0H0M0S" # 1 day before end e.alarm do |a| a.action = "AUDIO" a.trigger = "-PT15M" a.append_attach "Basso" end end ``` #### Output #### # BEGIN:VALARM # ACTION:EMAIL # ATTACH;FMTTYPE=application/binary:ftp://host.com/novo-procs/felizano.exe # TRIGGER:-PT15M # SUMMARY:Alarm notification # DESCRIPTION:This is an event reminder # ATTENDEE:mailto:me-too@my-domain.com # ATTENDEE:mailto:me-three@my-domain.com # END:VALARM # # BEGIN:VALARM # ACTION:DISPLAY # TRIGGER:-P1DT0H0M0S # SUMMARY:Alarm notification # END:VALARM # # BEGIN:VALARM # ACTION:AUDIO # ATTACH;VALUE=URI:Basso # TRIGGER:-PT15M # END:VALARM #### Checking for an Alarm #### Calling the `event.alarm` method will create an alarm if one doesn't exist. To check if an event has an alarm use the `has_alarm?` method. ```ruby event.has_alarm? # => false event.alarm # => # event.has_alarm? #=> true ``` TIMEZONES --- ```ruby cal = Icalendar::Calendar.new cal.timezone do |t| t.tzid = "America/Chicago" t.daylight do |d| d.tzoffsetfrom = "-0600" d.tzoffsetto = "-0500" d.tzname = "CDT" d.dtstart = "19700308T020000" d.rrule = "FREQ=YEARLY;BYMONTH=3;BYDAY=2SU" end t.standard do |s| s.tzoffsetfrom = "-0500" s.tzoffsetto = "-0600" s.tzname = "CST" s.dtstart = "19701101T020000" s.rrule = "FREQ=YEARLY;BYMONTH=11;BYDAY=1SU" end end ``` #### Output #### # BEGIN:VTIMEZONE # TZID:America/Chicago # BEGIN:DAYLIGHT # TZOFFSETFROM:-0600 # TZOFFSETTO:-0500 # TZNAME:CDT # DTSTART:19700308T020000 # RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU # END:DAYLIGHT # BEGIN:STANDARD # TZOFFSETFROM:-0500 # TZOFFSETTO:-0600 # TZNAME:CST # DTSTART:19701101T020000 # RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU # END:STANDARD # END:VTIMEZONE iCalendar has some basic support for creating VTIMEZONE blocks from timezone information pulled from `tzinfo`. You must require `tzinfo` support manually to take advantage. iCalendar has been tested and works with `tzinfo` versions 0.3, 1.x, and 2.x. The `tzinfo-data` gem may also be required depending on your version of `tzinfo` and potentially your operating system. #### Example #### ```ruby require 'icalendar/tzinfo' cal = Icalendar::Calendar.new event_start = DateTime.new 2008, 12, 29, 8, 0, 0 event_end = DateTime.new 2008, 12, 29, 11, 0, 0 tzid = "America/Chicago" tz = TZInfo::Timezone.get tzid timezone = tz.ical_timezone event_start cal.add_timezone timezone cal.event do |e| e.dtstart = Icalendar::Values::DateTime.new event_start, 'tzid' => tzid e.dtend = Icalendar::Values::DateTime.new event_end, 'tzid' => tzid e.summary = "Meeting with the man." e.description = "Have a long lunch meeting and decide nothing..." e.organizer = "mailto:jsmith@example.com" e.organizer = Icalendar::Values::CalAddress.new("mailto:jsmith@example.com", cn: 'John Smith') end ``` Parsing iCalendars --- ```ruby # Open a file or pass a string to the parser cal_file = File.open("single_event.ics") # Parser returns an array of calendars because a single file # can have multiple calendars. cals = Icalendar::Calendar.parse(cal_file) cal = cals.first # Now you can access the cal object in just the same way I created it event = cal.events.first puts "start date-time: #{event.dtstart}" puts "start date-time timezone: #{event.dtstart.ical_params['tzid']}" puts "summary: #{event.summary}" ``` You can also create a `Parser` instance directly, this can be used to enable strict parsing: ```ruby # Sometimes you want to strongly verify only rfc-approved properties are # used strict_parser = Icalendar::Parser.new(cal_file, true) cal = strict_parser.parse ``` Parsing Components (e.g. Events) --- ```ruby # Open a file or pass a string to the parser event_file = File.open("event.ics") # Parser returns an array of events because a single file # can have multiple events. events = Icalendar::Event.parse(event_file) event = events.first puts "start date-time: #{event.dtstart}" puts "start date-time timezone: #{event.dtstart.ical_params['tzid']}" puts "summary: #{event.summary}" ``` Finders --- Often times in web apps and other interactive applications you'll need to lookup items in a calendar to make changes or get details. Now you can find everything by the unique id automatically associated with all components. ```ruby cal = Calendar.new 10.times { cal.event } # Create 10 events with only default data. some_event = cal.events[5] # Grab it from the array of events # Use the uid as the key in your app key = some_event.uid # so later you can find it. same_event = cal.find_event(key) ``` Examples --- Check the unit tests for examples of most things you'll want to do, but please send me example code or let me know what's missing. Download --- The latest release version of this library can be found at * Installation --- It's all about rubygems: $ gem install icalendar Testing --- To run the tests: $ bundle install $ rake spec License --- This library is released under the same license as Ruby itself. Support & Contributions --- Please submit pull requests from a rebased topic branch and include tests for all bugs and features. Contributor Code of Conduct --- As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion. Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.1.0, available at [http://contributor-covenant.org/version/1/1/0/](http://contributor-covenant.org/version/1/1/0/) icalendar-2.10.3/CHANGELOG.md0000644000004100000410000001753314715202173015373 0ustar www-datawww-data## Unreleased ## 2.10.3 - 2024-09-21 - Override Icalendar::Value.== so that value objects can be compared to each other. - Correctly load activesupport before activesupport/time - Load ostruct to address deprecation warning - aki77 ## 2.10.2 - 2024-07-21 - Raise Icalendar::Parser::ParseError on bad .ics file input instead of RuntimeError - Micah Geisel - Remove Ruby 3.0 from the test matrix - still should work for now with ice_cube < 0.17.0 - Add Ruby 3.3 to the test matrix - Move from old History.txt file to modern CHANGELOG.md - Guillaume Briday ## 2.10.1 - 2023-12-01 - Parsing now handles VTIMEZONE blocks defined after their TZID is used in events and other components ## 2.10.0 - 2023-11-01 - Add changelog metadata to gemspec - Juri Hahn - Attempt to rescue timezone info when given a nonstandard tzid with no VTIMEZONE - Move Values classes that shouldn't be directly used into Helpers module ## 2.9.0 - 2023-08-11 - Always include the VALUE of Event URLs for improved compatibility - Sean Kelley - Improved parse performance - Thomas Cannon - Add helper methods for other Calendar method verbs - bugfix: Require stringio before use in Parser - vwyu ## 2.8.0 - 2022-07-10 - Fix compatibility with ActiveSupport 7 - Pat Allan - Set default action of "DISPLAY" on alarms - Rikson - Add license information to gemspec - Robert Reiz - Support RFC7986 properties - Daniele Frisanco ## 2.7.1 - 2021-03-14 - Recover from bad line-wrapping code that splits in the middle of Unicode code points - Add a verbose option to the Parser to quiet some of the chattier log entries ## 2.7.0 - 2020-09-12 - Handle custom component names, with and without X- prefix - Fix Component lookup to avoid namespace collisions ## 2.6.1 - 2019-12-07 - Improve performance when generating large ICS files - Alex Balhatchet ## 2.6.0 - 2019-11-26 - Improve performance for calculating timezone offsets - Justin Howard - Make it possible to de/serialize with Marshal - Pawel Niewiadomski - Avoid FrozenError when running with frozen_string_literal - Update minimum Ruby version to supported versions ## 2.5.3 - 2019-03-04 - Improve parsing performance - nehresma - Support tzinfo 2.0 - Misty De Meo ## 2.5.2 - 2018-12-08 - Remove usage of the global TimezoneStore instance, in favor of a local variable in the parser - Deprecate TimezoneStore class methods ## 2.5.1 - 2018-10-30 - Fix usage without ActiveSupport installed. ## 2.5.0 - 2018-09-10 - Set timezone information from VTIMEZONE components in cases that ActiveSupport can't figure it out (or isn't installed) - Prevent rewinding the Parser IO input during parsing - Niels Laukens - Update tested/supported ruby versions & documentation updates. ## 2.4.1 - 2016-09-03 - Fix parsing multiple calendars or components in same file - Patrick Schnetger - Fix multi-byte folding bug - Niels Laukens - Fix typos across the code - Martin Edenhofer & yuuji.yaginuma ## 2.4.0 - 2016-07-04 - Enable parsing individual ICalendar components - Patrick Schnetger - many bug fixes. Thanks to Quan Sun, Garry Shutler, Ryan Bigg, Patrick Schnetger and others - README/documentation updates. Thanks to JonMidhir and Hendrik Sollich ## 2.3.0 - 2015-04-26 - fix value parameter for properties with multiple values - fix error when assigning Icalendar::Values::Array to a component - Fall back to Icalendar::Values::Date if Icalendar::Values::DateTime is not given a properly formatted value - Downcase the keys in ical_params to ensure we aren't assigning both tzid and TZID ## 2.2.2 - 2014-12-27 - add a `has_#{component}?` method for testing if component exists - John Hope - add documentation & tests for organizer attribute - Ben Walding ## 2.2.1 - 2014-12-03 - Prevent crashes when using ActiveSupport::TimeWithZone in multi-property DateTime fields - Danny (tdg5) - Ensure TimeWithZone is loaded before using, not just ActiveSupport - Jeremy Evans - Improve error message on unparseable DateTimes - Garry Shutler ## 2.2.0 - 2014-09-23 - Default to non-strict parsing - Enable sorting events by dtstart - Mark Rickert - Better tolerate malformed lines in parser - Garry Shutler - Deduplicate timezone code - Jan Vlnas - Eliminate warnings - rochefort ## 2.1.2 - 2014-09-10 - Fix timezone abbreviation generation - Jan Vlnas - Fix timezone repeat rules for end of month ## 2.1.1 - 2014-07-23 - Quiet TimeWithZone support logging - Use SecureRandom.uuid - antoinelyset ## 2.1.0 - 2014-06-17 - Enable parsing all custom properties, not just X- prefixed ones Requires non-strict parsing - Fixed bugs when using non-MRI ruby interpreters - Fix bug copying OpenStruct-backed value types ## 2.0.1 - 2014-04-27 - Re-add support for ruby 1.9.2 ## 2.0.0 - 2014-04-22 - Add Icalendar.logger class & logging in Parser - Support tzinfo ~> 0.3 and ~> 1.1 ## 2.0.0.beta.2 - 2014-04-11 - Add uid & acknowledged fields from valarm extensions - Swallow NoMethodError on non-strict parsers - Expose a parse_property method on Icalendar::Parser ## 2.0.0.beta.1 - 2014-03-30 - Rewrite for easier development going forward. ## 1.5.2 - 2014-02-22 - Output timezone components first - Fix undefined local variable or method 'e' - Jason Stirk ## 1.5.1 - 2014-02-27 - Check for dtend existence before setting timezone - Jonas Grau - Clean up and refactor components - Kasper Timm Hansen ## 1.5.0 - 2013-12-06 - Support for custom x- properties - Jake Craige ## 1.4.5 - 2013-11-14 - Fix Geo accessor methods - bouzuya - Add ical_multiline_property :related_to for Alarm - Allow using multi setters to append single values ## 1.4.4 - 2013-11-05 - Allow user to handle TZInfo::AmbiguousTime error - David Bradford - Better handling of multiple exdate and rdate values ## 1.4.3 - 2013-09-18 - Fix concatenation of multiple BYWEEK or BYMONTH recurrence rules ## 1.4.2 - 2013-09-11 - Double Quote parameter values that contain forbidden characters - Update Component#respond_to? to match Ruby 2.0 - Keith Marcum ## 1.4.1 - 2013-06-25 - Don't escape semicolon in GEO property - temirov - Allow access to various parts of RRule class ## 1.4.0 - 2013-05-21 - Implement ACKNOWLEDGED property for VALARM - tsuzuki08 - Output VERSION property as first line after BEGIN:VCALENDAR - Check for unbounded timezone transitions in tzinfo ## 1.3.0 - 2013-03-31 - Lenient parsing mode ignores unknown properties - David Grandinetti - VTIMEZONE positive offsets properly have "+" prepended (Fixed issue #18) - Benjamin Jorgensen (sorry for misspelling your last name) ## 1.2.4 - 2013-03-26 - Proxy component values now frozen in Ruby 2.0 (Fixed issue #17) - Clean up gemspec for cleaner installing via bundler/git ## 1.2.3 - 2013-03-09 - Call `super` from Component#method_missing - Clean up warnings in test suite - Add Gemfile for installing development dependencies ## 1.2.2 - 2013-02-16 - added TZURL property to Timezone component - spacepixels - correct days in RRule ("[TU,WE]" -> "TU,WE") - Christoph Finkensiep ## 1.2.1 - 2012-11-12 - Adds uid property to alarms to support iCloud - Jeroen Jacobs - Fix up testing docs - Eric Carty-Fickes - Fix parsing property params that have : in them - Sean Dague - Clean up warnings in test suite - Sean Dague ## 1.2.0 - 2012-08-30 * Fix calendar handling for dates < 1000 - Ryan Ahearn * Updated license to GPL/BSD/Ruby at users option ## 1.1.6 - 2011-03-10 * Fix todo handling (thanks to Frank Schwarz) * clean up a number of warnings during test runs ## 1.1.5 - 2010-06-21 * Fix for windows line endings (thanks to Rowan Collins) ## 1.1.4 - 2010-04-23 * Fix for RRULE escaping * Fix tests so they run under 1.8.7 in multiple environments * Readme fix ## 1.1.3 - 2010-03-15 * Revert component sorting behavior that I was trying to make the tests run more consistantly on different platforms. * Added new test for multiple events in a calendar which caught that break. ## 1.1.2 - 2010-03-10 * Convert project to newgem to make for easier publishing