1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
1.2 +++ b/tests/test_calendar.py Sat Oct 18 01:41:15 2008 +0200
1.3 @@ -0,0 +1,8 @@
1.4 +#!/usr/bin/env python
1.5 +
1.6 +import codecs, vCalendar
1.7 +
1.8 +f = codecs.open("test.ics", encoding="utf-8")
1.9 +doc = vCalendar.parse(f)
1.10 +
1.11 +# vim: tabstop=4 expandtab shiftwidth=4
2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2.2 +++ b/tests/test_card.py Sat Oct 18 01:41:15 2008 +0200
2.3 @@ -0,0 +1,8 @@
2.4 +#!/usr/bin/env python
2.5 +
2.6 +import codecs, vContent
2.7 +
2.8 +f = codecs.open("test.vcf", encoding="utf-8")
2.9 +doc = vContent.parse(f)
2.10 +
2.11 +# vim: tabstop=4 expandtab shiftwidth=4
3.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
3.2 +++ b/tests/test_reader.py Sat Oct 18 01:41:15 2008 +0200
3.3 @@ -0,0 +1,48 @@
3.4 +#!/usr/bin/env python
3.5 +
3.6 +import vContent
3.7 +import StringIO
3.8 +
3.9 +s = StringIO.StringIO("""PROP:p1=v1;p2
3.10 +=v2;p21;p3=v3;"p4"="v4";"p5=v5
3.11 +;p5=v5":"hello
3.12 +world"
3.13 +""")
3.14 +
3.15 +r = vContent.Reader(s)
3.16 +
3.17 +data = r.read_until(r.SEPARATORS)
3.18 +print data
3.19 +assert data == ("PROP", ":")
3.20 +data = r.read_until(r.SEPARATORS_PLUS_EQUALS)
3.21 +print data
3.22 +assert data == ("p1", "=")
3.23 +data = r.read_until(r.SEPARATORS)
3.24 +print data
3.25 +assert data == ("v1", ";")
3.26 +data = r.read_until(r.SEPARATORS_PLUS_EQUALS)
3.27 +print data
3.28 +assert data == ("p2\n", "=")
3.29 +data = r.read_until(r.SEPARATORS)
3.30 +print data
3.31 +assert data == ("v2", ";")
3.32 +data = r.read_until(r.SEPARATORS_PLUS_EQUALS)
3.33 +print data
3.34 +assert data == ("p21", ";")
3.35 +data = r.read_until(r.SEPARATORS_PLUS_EQUALS)
3.36 +print data
3.37 +assert data == ("p3", "=")
3.38 +data = r.read_until(r.SEPARATORS)
3.39 +print data
3.40 +assert data == ("v3", ";")
3.41 +data = r.read_until(r.SEPARATORS_PLUS_EQUALS)
3.42 +print data
3.43 +assert data == ('"p4"', "=")
3.44 +data = r.read_until(r.SEPARATORS)
3.45 +print data
3.46 +assert data == ('"v4"', ";")
3.47 +data = r.read_until(r.SEPARATORS_PLUS_EQUALS)
3.48 +print data
3.49 +assert data == ('"p5=v5\n;p5=v5"', ":")
3.50 +
3.51 +# vim: tabstop=4 expandtab shiftwidth=4
4.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
4.2 +++ b/vCalendar.py Sat Oct 18 01:41:15 2008 +0200
4.3 @@ -0,0 +1,100 @@
4.4 +#!/usr/bin/env python
4.5 +
4.6 +"""
4.7 +Parsing of vCalendar and iCalendar files.
4.8 +
4.9 +Copyright (C) 2008 Paul Boddie <paul@boddie.org.uk>
4.10 +
4.11 +This program is free software; you can redistribute it and/or modify it under
4.12 +the terms of the GNU Lesser General Public License as published by the Free
4.13 +Software Foundation; either version 3 of the License, or (at your option) any
4.14 +later version.
4.15 +
4.16 +This program is distributed in the hope that it will be useful, but WITHOUT
4.17 +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
4.18 +FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
4.19 +details.
4.20 +
4.21 +You should have received a copy of the GNU Lesser General Public License along
4.22 +with this program. If not, see <http://www.gnu.org/licenses/>.
4.23 +
4.24 +--------
4.25 +
4.26 +References:
4.27 +
4.28 +RFC 2445: Internet Calendaring and Scheduling Core Object Specification
4.29 + (iCalendar)
4.30 + http://rfc.net/rfc2445.html
4.31 +"""
4.32 +
4.33 +from vContent import Parser, Reader
4.34 +
4.35 +try:
4.36 + set
4.37 +except NameError:
4.38 + from sets import Set as set
4.39 +
4.40 +class vCalendarParser(Parser):
4.41 +
4.42 + "A parser specifically for vCalendar/iCalendar."
4.43 +
4.44 + quoted_parameters = set([
4.45 + "ALTREP", "DELEGATED-FROM", "DELEGATED-TO", "DIR", "MEMBER", "SENT-BY"
4.46 + ])
4.47 + multivalued_parameters = set([
4.48 + "DELEGATED-FROM", "DELEGATED-TO", "MEMBER"
4.49 + ])
4.50 + quoted_types = set(["URI"])
4.51 +
4.52 + def decode_quoted_value(self, value):
4.53 +
4.54 + "Decode the given 'value', returning a list of decoded values."
4.55 +
4.56 + if value[0] == '"' and value[-1] == '"':
4.57 + return value[1:-1]
4.58 + else:
4.59 + return value
4.60 +
4.61 + def handleProperty(self, name, parameters, value):
4.62 +
4.63 + """
4.64 + Record the property with the given 'name', 'parameters' and 'value' as
4.65 + part of the current component's children.
4.66 + """
4.67 +
4.68 + decoded_parameters = {}
4.69 + for param_name, param_value in parameters.items():
4.70 + if param_name in self.quoted_parameters:
4.71 + param_value = self.decode_quoted_value(param_value)
4.72 + separator = '","'
4.73 + else:
4.74 + separator = ","
4.75 + if param_name in self.multivalued_parameters:
4.76 + param_value = param_value.split(separator)
4.77 + decoded_parameters[param_name] = param_value
4.78 +
4.79 + return Parser.handleProperty(self, name, decoded_parameters, value)
4.80 +
4.81 +# Public functions.
4.82 +
4.83 +def parse(f, non_standard_newline=0):
4.84 +
4.85 + """
4.86 + Parse the resource data found through the use of the file object 'f', which
4.87 + should provide Unicode data, and put the resource information in the given
4.88 + 'store'. (The codecs module can be used to open files or to wrap streams in
4.89 + order to provide Unicode data.)
4.90 +
4.91 + The optional 'non_standard_newline' can be set to a true value (unlike the
4.92 + default) in order to attempt to process files with CR as the end of line
4.93 + character.
4.94 +
4.95 + As a result of parsing the resource, the root node of the imported resource
4.96 + is returned.
4.97 + """
4.98 +
4.99 + reader = Reader(f, non_standard_newline=non_standard_newline)
4.100 + parser = vCalendarParser()
4.101 + return parser.parse(reader)
4.102 +
4.103 +# vim: tabstop=4 expandtab shiftwidth=4
5.1 --- a/vContent.py Fri Oct 17 23:12:25 2008 +0200
5.2 +++ b/vContent.py Sat Oct 18 01:41:15 2008 +0200
5.3 @@ -33,16 +33,28 @@
5.4 http://rfc.net/rfc2426.html
5.5 """
5.6
5.7 +try:
5.8 + set
5.9 +except NameError:
5.10 + from sets import Set as set
5.11 +
5.12 # Encoding-related imports.
5.13
5.14 import base64, quopri
5.15
5.16 +# Tokenisation help.
5.17 +
5.18 +import re
5.19 +
5.20 # Simple reader class.
5.21
5.22 class Reader:
5.23
5.24 "A simple class wrapping a file, providing simple pushback capabilities."
5.25
5.26 + SEPARATORS = re.compile('[;:"]')
5.27 + SEPARATORS_PLUS_EQUALS = re.compile('[=;:"]')
5.28 +
5.29 def __init__(self, f, non_standard_newline=0):
5.30
5.31 """
5.32 @@ -95,8 +107,6 @@
5.33 was found, return the entire string together with a target of None.
5.34 """
5.35
5.36 - indexes = {}
5.37 -
5.38 # Remember the entire text read and the index of the current line in
5.39 # that text.
5.40
5.41 @@ -106,40 +116,59 @@
5.42 lines.append(line)
5.43 start = 0
5.44
5.45 - while indexes == {} and line != "":
5.46 - for target in targets:
5.47 - index = line.find(target)
5.48 + # Remember the first target.
5.49 +
5.50 + first = None
5.51 + first_pos = None
5.52 + in_quoted_region = 0
5.53
5.54 - # Always choose the first matching target.
5.55 + # Process each line, looking for the targets.
5.56 +
5.57 + while line != "":
5.58 + match = targets.search(line, start)
5.59 +
5.60 + # Where nothing matches, get the next line.
5.61
5.62 - if index != -1 and not indexes.has_key(start + index):
5.63 - indexes[start + index] = target
5.64 + if match is None:
5.65 + line = self.readline()
5.66 + lines.append(line)
5.67 + start = 0
5.68
5.69 - start += len(line)
5.70 - line = self.readline()
5.71 - lines.append(line)
5.72 + # Where a double quote matches, toggle the region state.
5.73
5.74 - text = "".join(lines)
5.75 + elif match.group() == '"':
5.76 + in_quoted_region = not in_quoted_region
5.77 + start = match.end()
5.78 +
5.79 + # Where something else matches outside a region, stop searching.
5.80
5.81 - if indexes:
5.82 - min_index = reduce(min, indexes.keys())
5.83 - target = indexes[min_index]
5.84 + elif not in_quoted_region:
5.85 + first = match.group()
5.86 + first_pos = match.start()
5.87 + break
5.88
5.89 - # Skip the target.
5.90 - # Since the end of the buffer should always be a newline, ignore the
5.91 - # last element.
5.92 + # Otherwise, keep looking for the end of the region.
5.93 +
5.94 + else:
5.95 + start = match.end()
5.96 +
5.97 + # Where no more input can provide the targets, return a special result.
5.98
5.99 - lines = text[min_index + len(target):].split("\n")[:]
5.100 - if not lines[-1]:
5.101 - del lines[-1]
5.102 - lines.reverse()
5.103 + else:
5.104 + text = "".join(lines)
5.105 + return text, None
5.106 +
5.107 + # Push back the text after the target.
5.108
5.109 - for line in lines:
5.110 - self.pushback(line + "\n")
5.111 + after_target = lines[-1][first_pos + len(first):]
5.112 + self.pushback(after_target)
5.113
5.114 - return text[:min_index], target
5.115 - else:
5.116 - return text, None
5.117 + # Produce the lines until the matching line, together with the portion
5.118 + # of the matching line before the target.
5.119 +
5.120 + lines[-1] = lines[-1][:first_pos]
5.121 + text = "".join(lines)
5.122 + return text, first
5.123
5.124 class StreamParser:
5.125
5.126 @@ -176,7 +205,7 @@
5.127 f = self.f
5.128
5.129 parameters = {}
5.130 - name, sep = f.read_until([";", ":"])
5.131 + name, sep = f.read_until(f.SEPARATORS)
5.132
5.133 name = name.strip()
5.134
5.135 @@ -187,11 +216,11 @@
5.136
5.137 # Find the actual modifier.
5.138
5.139 - parameter_name, sep = f.read_until(["=", ";", ":"])
5.140 + parameter_name, sep = f.read_until(f.SEPARATORS_PLUS_EQUALS)
5.141 parameter_name = parameter_name.strip()
5.142
5.143 if sep == "=":
5.144 - parameter_value, sep = f.read_until([";", ":"])
5.145 + parameter_value, sep = f.read_until(f.SEPARATORS)
5.146 parameter_value = parameter_value.strip()
5.147 else:
5.148 parameter_value = None
5.149 @@ -313,7 +342,7 @@
5.150 def handleProperty(self, name, parameters, value):
5.151
5.152 """
5.153 - Record the component with the given 'name', 'parameters' and 'value' as
5.154 + Record the property with the given 'name', 'parameters' and 'value' as
5.155 part of the current component's children.
5.156 """
5.157