You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.

376 lines
10 KiB

=begin
Copyright (C) 2005 Jeff Rose
Copyright (C) 2005 Sam Roberts
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
require 'date'
require 'uri'
require 'stringio'
module Icalendar
def Icalendar.parse(src, single = false)
cals = Icalendar::Parser.new(src).parse
if single
cals.first
else
cals
end
end
class Parser < Icalendar::Base
# date = date-fullyear ["-"] date-month ["-"] date-mday
# date-fullyear = 4 DIGIT
# date-month = 2 DIGIT
# date-mday = 2 DIGIT
DATE = '(\d\d\d\d)-?(\d\d)-?(\d\d)'
# time = time-hour [":"] time-minute [":"] time-second [time-secfrac] [time-zone]
# time-hour = 2 DIGIT
# time-minute = 2 DIGIT
# time-second = 2 DIGIT
# time-secfrac = "," 1*DIGIT
# time-zone = "Z" / time-numzone
# time-numzome = sign time-hour [":"] time-minute
TIME = '(\d\d):?(\d\d):?(\d\d)(\.\d+)?(Z|[-+]\d\d:?\d\d)?'
def initialize(src)
# Setup the parser method hash table
setup_parsers()
if src.respond_to?(:gets)
@file = src
elsif (not src.nil?) and src.respond_to?(:to_s)
@file = StringIO.new(src.to_s, 'r')
else
raise ArgumentError, "CalendarParser.new cannot be called with a #{src.class} type!"
end
@prev_line = @file.gets
@prev_line.chomp! unless @prev_line.nil?
@@logger.debug("New Calendar Parser: #{@file.inspect}")
end
# Define next line for an IO object.
# Works for strings now with StringIO
def next_line
line = @prev_line
if line.nil?
return nil
end
# Loop through until we get to a non-continuation line...
loop do
nextLine = @file.gets
@@logger.debug "new_line: #{nextLine}"
if !nextLine.nil?
nextLine.chomp!
end
# If it's a continuation line, add it to the last.
# If it's an empty line, drop it from the input.
if( nextLine =~ /^[ \t]/ )
line << nextLine[1, nextLine.size]
elsif( nextLine =~ /^$/ )
else
@prev_line = nextLine
break
end
end
line
end
# Parse the calendar into an object representation
def parse
calendars = []
@@logger.debug "parsing..."
# Outer loop for Calendar objects
while (line = next_line)
fields = parse_line(line)
# Just iterate through until we find the beginning of a calendar object
if fields[:name] == "BEGIN" and fields[:value] == "VCALENDAR"
cal = parse_component
@@logger.debug "Added parsed calendar..."
calendars << cal
end
end
calendars
end
private
# Parse a single VCALENDAR object
# -- This should consist of the PRODID, VERSION, option METHOD & CALSCALE,
# and then one or more calendar components: VEVENT, VTODO, VJOURNAL,
# VFREEBUSY, VTIMEZONE
def parse_component(component = Calendar.new)
@@logger.debug "parsing new component..."
while (line = next_line)
fields = parse_line(line)
name = fields[:name].upcase
# Although properties are supposed to come before components, we should
# be able to handle them in any order...
if name == "END"
break
elsif name == "BEGIN" # New component
case(fields[:value])
when "VEVENT" # Event
component.add_component parse_component(Event.new)
when "VTODO" # Todo entry
component.add_component parse_component(Todo.new)
when "VALARM" # Alarm sub-component for event and todo
component.add_component parse_component(Alarm.new)
when "VJOURNAL" # Journal entry
component.add_component parse_component(Journal.new)
when "VFREEBUSY" # Free/Busy section
component.add_component parse_component(Freebusy.new)
when "VTIMEZONE" # Timezone specification
component.add_component parse_component(Timezone.new)
when "STANDARD" # Standard time sub-component for timezone
component.add_component parse_component(Standard.new)
when "DAYLIGHT" # Daylight time sub-component for timezone
component.add_component parse_component(Daylight.new)
else # Uknown component type, skip to matching end
until ((line = next_line) == "END:#{fields[:value]}"); end
next
end
else # If its not a component then it should be a property
params = fields[:params]
value = fields[:value]
# Lookup the property name to see if we have a string to
# object parser for this property type.
if @parsers.has_key?(name)
value = @parsers[name].call(name, params, value)
end
name = name.downcase
# TODO: check to see if there are any more conflicts.
if name == 'class' or name == 'method'
name = "ip_" + name
end
# Replace dashes with underscores
name = name.gsub('-', '_')
if component.multi_property?(name)
adder = "add_" + name
if component.respond_to?(adder)
component.send(adder, value, params)
else
raise(UnknownPropertyMethod, "Unknown property type: #{adder}")
end
else
if component.respond_to?(name)
component.send(name, value, params)
else
raise(UnknownPropertyMethod, "Unknown property type: #{name}")
end
end
end
end
component
end
# 1*(ALPHA / DIGIT / "=")
NAME = '[-a-z0-9]+'
# <"> <Any character except CTLs, DQUOTE> <">
QSTR = '"[^"]*"'
# Contentline
LINE = "(#{NAME})(.*(?:#{QSTR})|(?:[^:]*))\:(.*)"
# *<Any character except CTLs, DQUOTE, ";", ":", ",">
PTEXT = '[^";:,]*'
# param-value = ptext / quoted-string
PVALUE = "#{QSTR}|#{PTEXT}"
# param = name "=" param-value *("," param-value)
PARAM = ";(#{NAME})(=?)((?:#{PVALUE})(?:,#{PVALUE})*)"
def parse_line(line)
unless line =~ %r{#{LINE}}i # Case insensitive match for a valid line
raise "Invalid line in calendar string!"
end
name = $1.upcase # The case insensitive part is upcased for easier comparison...
paramslist = $2
value = $3.gsub("\\;", ";").gsub("\\,", ",").gsub("\\n", "\n").gsub("\\\\", "\\")
# Parse the parameters
params = {}
if paramslist.size > 1
paramslist.scan( %r{#{PARAM}}i ) do
# parameter names are case-insensitive, and multi-valued
pname = $1
pvals = $3
# If their isn't an '=' sign then we need to do some custom
# business. Defaults to 'type'
if $2 == ""
pvals = $1
case $1
when /quoted-printable/i
pname = 'encoding'
when /base64/i
pname = 'encoding'
else
pname = 'type'
end
end
# Make entries into the params dictionary where the name
# is the key and the value is an array of values.
unless params.key? pname
params[pname] = []
end
# Save all the values into the array.
pvals.scan( %r{(#{PVALUE})} ) do
if $1.size > 0
params[pname] << $1
end
end
end
end
{:name => name, :value => value, :params => params}
end
## Following is a collection of parsing functions for various
## icalendar property value data types... First we setup
## a hash with property names pointing to methods...
def setup_parsers
@parsers = {}
# Integer properties
m = self.method(:parse_integer)
@parsers["PERCENT-COMPLETE"] = m
@parsers["PRIORITY"] = m
@parsers["REPEAT"] = m
@parsers["SEQUENCE"] = m
# Dates and Times
m = self.method(:parse_datetime)
@parsers["COMPLETED"] = m
@parsers["DTEND"] = m
@parsers["DUE"] = m
@parsers["DTSTART"] = m
@parsers["RECURRENCE-ID"] = m
@parsers["EXDATE"] = m
@parsers["RDATE"] = m
@parsers["CREATED"] = m
@parsers["DTSTAMP"] = m
@parsers["LAST-MODIFIED"] = m
# URI's
m = self.method(:parse_uri)
@parsers["TZURL"] = m
@parsers["ATTENDEE"] = m
@parsers["ORGANIZER"] = m
@parsers["URL"] = m
# This is a URI by default, and if its not a valid URI
# it will be returned as a string which works for binary data
# the other possible type.
@parsers["ATTACH"] = m
# GEO
m = self.method(:parse_geo)
@parsers["GEO"] = m
end
# Booleans
# NOTE: It appears that although this is a valid data type
# there aren't any properties that use it... Maybe get
# rid of this in the future.
def parse_boolean(name, params, value)
if value.upcase == "FALSE"
false
else
true
end
end
# Dates, Date-Times & Times
# NOTE: invalid dates & times will be returned as strings...
def parse_datetime(name, params, value)
begin
DateTime.parse(value)
rescue Exception
value
end
end
# Durations
# TODO: Need to figure out the best way to represent durations
# so just returning string for now.
def parse_duration(name, params, value)
value
end
# Floats
# NOTE: returns 0.0 if it can't parse the value
def parse_float(name, params, value)
value.to_f
end
# Integers
# NOTE: returns 0 if it can't parse the value
def parse_integer(name, params, value)
value.to_i
end
# Periods
# TODO: Got to figure out how to represent periods also...
def parse_period(name, params, value)
value
end
# Calendar Address's & URI's
# NOTE: invalid URI's will be returned as strings...
def parse_uri(name, params, value)
begin
URI.parse(value)
rescue Exception
value
end
end
# Geographical location (GEO)
# NOTE: returns an array with two floats (long & lat)
# if the parsing fails return the string
def parse_geo(name, params, value)
strloc = value.split(';')
if strloc.size != 2
return value
end
Geo.new(strloc[0].to_f, strloc[1].to_f)
end
end
end