1.1 --- a/vContent.py Sun Nov 02 04:06:23 2008 +0100
1.2 +++ b/vContent.py Sun Nov 02 23:06:25 2008 +0100
1.3 @@ -52,9 +52,6 @@
1.4
1.5 "A simple class wrapping a file, providing simple pushback capabilities."
1.6
1.7 - SEPARATORS = re.compile('[;:"]')
1.8 - SEPARATORS_PLUS_EQUALS = re.compile('[=;:"]')
1.9 -
1.10 def __init__(self, f, non_standard_newline=0):
1.11
1.12 """
1.13 @@ -66,7 +63,7 @@
1.14 self.f = f
1.15 self.non_standard_newline = non_standard_newline
1.16 self.lines = []
1.17 - self.line_number = 0
1.18 + self.line_number = 1 # about to read line 1
1.19
1.20 def pushback(self, line):
1.21
1.22 @@ -98,23 +95,69 @@
1.23 else:
1.24 return line
1.25
1.26 - def read_until(self, targets):
1.27 + def read_content_line(self):
1.28
1.29 """
1.30 - Read from the stream until one of the 'targets' is seen. Return the
1.31 - string from the current position up to the target found, along with the
1.32 - target string, using a tuple of the form (string, target). If no target
1.33 - was found, return the entire string together with a target of None.
1.34 + Read an entire content line, itself potentially consisting of many
1.35 + physical lines of text.
1.36 """
1.37
1.38 - # Remember the entire text read and the index of the current line in
1.39 - # that text.
1.40 + line = self.readline()
1.41
1.42 - lines = []
1.43 + # Strip all appropriate whitespace from the right end of each line.
1.44 + # For subsequent lines, remove the first whitespace character.
1.45 + # See section 4.1 of the iCalendar specification.
1.46 +
1.47 + lines = [line.rstrip("\r\n")]
1.48
1.49 line = self.readline()
1.50 - lines.append(line)
1.51 - start = 0
1.52 + while line.startswith(" ") or line.startswith("\t"):
1.53 + lines.append(line[1:].rstrip("\r\n"))
1.54 + line = self.readline()
1.55 +
1.56 + # Since one line too many will have been read, push the line back into
1.57 + # the file.
1.58 +
1.59 + if line:
1.60 + self.pushback(line)
1.61 +
1.62 + return "".join(lines)
1.63 +
1.64 + def get_content_line(self):
1.65 +
1.66 + "Return a content line object for the current line."
1.67 +
1.68 + return ContentLine(self.read_content_line())
1.69 +
1.70 +class ContentLine:
1.71 +
1.72 + "A content line which can be searched."
1.73 +
1.74 + SEPARATORS = re.compile('[;:"]')
1.75 + SEPARATORS_PLUS_EQUALS = re.compile('[=;:"]')
1.76 +
1.77 + def __init__(self, text):
1.78 + self.text = text
1.79 + self.start = 0
1.80 +
1.81 + def get_remaining(self):
1.82 +
1.83 + "Get the remaining text from the content line."
1.84 +
1.85 + return self.text[self.start:]
1.86 +
1.87 + def search(self, targets):
1.88 +
1.89 + """
1.90 + Find one of the 'targets' in the text, returning the string from the
1.91 + current position up to the target found, along with the target string,
1.92 + using a tuple of the form (string, target). If no target was found,
1.93 + return the entire string together with a target of None.
1.94 + """
1.95 +
1.96 + text = self.text
1.97 + start = pos = self.start
1.98 + length = len(text)
1.99
1.100 # Remember the first target.
1.101
1.102 @@ -122,23 +165,21 @@
1.103 first_pos = None
1.104 in_quoted_region = 0
1.105
1.106 - # Process each line, looking for the targets.
1.107 + # Process the text, looking for the targets.
1.108
1.109 - while line != "":
1.110 - match = targets.search(line, start)
1.111 + while pos < length:
1.112 + match = targets.search(text, pos)
1.113
1.114 - # Where nothing matches, get the next line.
1.115 + # Where nothing matches, end the search.
1.116
1.117 if match is None:
1.118 - line = self.readline()
1.119 - lines.append(line)
1.120 - start = 0
1.121 + pos = length
1.122
1.123 # Where a double quote matches, toggle the region state.
1.124
1.125 elif match.group() == '"':
1.126 in_quoted_region = not in_quoted_region
1.127 - start = match.end()
1.128 + pos = match.end()
1.129
1.130 # Where something else matches outside a region, stop searching.
1.131
1.132 @@ -150,25 +191,16 @@
1.133 # Otherwise, keep looking for the end of the region.
1.134
1.135 else:
1.136 - start = match.end()
1.137 + pos = match.end()
1.138
1.139 # Where no more input can provide the targets, return a special result.
1.140
1.141 else:
1.142 - text = "".join(lines)
1.143 - return text, None
1.144 -
1.145 - # Push back the text after the target.
1.146 + self.start = length
1.147 + return text[start:], None
1.148
1.149 - after_target = lines[-1][first_pos + len(first):]
1.150 - self.pushback(after_target)
1.151 -
1.152 - # Produce the lines until the matching line, together with the portion
1.153 - # of the matching line before the target.
1.154 -
1.155 - lines[-1] = lines[-1][:first_pos]
1.156 - text = "".join(lines)
1.157 - return text, first
1.158 + self.start = match.end()
1.159 + return text[start:first_pos], first
1.160
1.161 class StreamParser:
1.162
1.163 @@ -211,24 +243,30 @@
1.164 """
1.165
1.166 f = self.f
1.167 + line_number = f.line_number
1.168 + line = f.get_content_line()
1.169
1.170 - parameters = {}
1.171 - name, sep = f.read_until(f.SEPARATORS)
1.172 + # Read the property name.
1.173
1.174 + name, sep = line.search(line.SEPARATORS)
1.175 name = name.strip()
1.176
1.177 if not name and sep is None:
1.178 raise StopIteration
1.179
1.180 + # Read the parameters.
1.181 +
1.182 + parameters = {}
1.183 +
1.184 while sep == ";":
1.185
1.186 # Find the actual modifier.
1.187
1.188 - parameter_name, sep = f.read_until(f.SEPARATORS_PLUS_EQUALS)
1.189 + parameter_name, sep = line.search(line.SEPARATORS_PLUS_EQUALS)
1.190 parameter_name = parameter_name.strip()
1.191
1.192 if sep == "=":
1.193 - parameter_value, sep = f.read_until(f.SEPARATORS)
1.194 + parameter_value, sep = line.search(line.SEPARATORS)
1.195 parameter_value = parameter_value.strip()
1.196 else:
1.197 parameter_value = None
1.198 @@ -240,27 +278,11 @@
1.199 # Get the value content.
1.200
1.201 if sep != ":":
1.202 - raise ValueError, f.line_number
1.203 -
1.204 - # Strip all appropriate whitespace from the right end of each line.
1.205 - # For subsequent lines, remove the first whitespace character.
1.206 - # See section 4.1 of the iCalendar specification.
1.207 + raise ValueError, line_number
1.208
1.209 - line = f.readline()
1.210 - value_lines = [line.rstrip("\r\n")]
1.211 - line = f.readline()
1.212 - while line != "" and line[0] in [" ", "\t"]:
1.213 - value_lines.append(line.rstrip("\r\n")[1:])
1.214 - line = f.readline()
1.215 + # Obtain and decode the value.
1.216
1.217 - # Since one line too many will have been read, push the line back into the
1.218 - # file.
1.219 -
1.220 - f.pushback(line)
1.221 -
1.222 - # Decode the value.
1.223 -
1.224 - value = self.decode(name, parameters, "".join(value_lines))
1.225 + value = self.decode(name, parameters, line.get_remaining())
1.226
1.227 return name, parameters, value
1.228
1.229 @@ -384,16 +406,65 @@
1.230
1.231 # Writer classes.
1.232
1.233 +class Writer:
1.234 +
1.235 + "A simple class wrapping a file, providing simple output capabilities."
1.236 +
1.237 + default_line_length = 76
1.238 +
1.239 + def __init__(self, f, line_length=None):
1.240 +
1.241 + """
1.242 + Initialise the object with the file 'f'. If 'line_length' is set, the
1.243 + length of written lines will conform to the specified value instead of
1.244 + the default value.
1.245 + """
1.246 +
1.247 + self.f = f
1.248 + self.line_length = line_length or self.default_line_length
1.249 + self.char_offset = 0
1.250 +
1.251 + def write(self, text):
1.252 +
1.253 + "Write the 'text' to the file."
1.254 +
1.255 + f = self.f
1.256 + line_length = self.line_length
1.257 +
1.258 + i = 0
1.259 + remaining = len(text)
1.260 +
1.261 + while remaining:
1.262 + space = line_length - self.char_offset
1.263 + if remaining > space:
1.264 + f.write(text[i:i + space])
1.265 + f.write("\r\n ")
1.266 + self.char_offset = 1
1.267 + i += space
1.268 + remaining -= space
1.269 + else:
1.270 + f.write(text[i:])
1.271 + self.char_offset += remaining
1.272 + i += remaining
1.273 + remaining = 0
1.274 +
1.275 + def end_line(self):
1.276 +
1.277 + "End the current content line."
1.278 +
1.279 + if self.char_offset > 0:
1.280 + self.char_offset = 0
1.281 + self.f.write("\r\n")
1.282 +
1.283 class StreamWriter:
1.284
1.285 "A stream writer for content in vCard/vCalendar/iCalendar-like formats."
1.286
1.287 - def __init__(self, f, line_length=76):
1.288 + def __init__(self, f):
1.289
1.290 "Initialise the parser for the given file 'f'."
1.291
1.292 self.f = f
1.293 - self.line_length = line_length
1.294
1.295 def write(self, name, parameters, value):
1.296
1.297 @@ -405,12 +476,14 @@
1.298 f = self.f
1.299
1.300 f.write(name)
1.301 - self.write_parameters(parameters)
1.302 + for parameter_name, parameter_value in parameters.items():
1.303 + f.write(";")
1.304 + f.write(parameter_name)
1.305 + f.write("=")
1.306 + f.write(parameter_value)
1.307 f.write(":")
1.308 -
1.309 - for line in self.fold(self.encode(name, parameters, value)):
1.310 - f.write(line)
1.311 - f.write("\r\n")
1.312 + f.write(self.encode(name, parameters, value))
1.313 + f.end_line()
1.314
1.315 def encode_content(self, value):
1.316
1.317 @@ -420,18 +493,6 @@
1.318
1.319 # Internal methods.
1.320
1.321 - def write_parameters(self, parameters):
1.322 -
1.323 - "Write the given 'parameters'."
1.324 -
1.325 - f = self.f
1.326 -
1.327 - for parameter_name, parameter_value in parameters.items():
1.328 - f.write(";")
1.329 - f.write(parameter_name)
1.330 - f.write("=")
1.331 - f.write(parameter_value)
1.332 -
1.333 def encode(self, name, parameters, value):
1.334
1.335 "Encode using 'name' and 'parameters' the given 'value'."
1.336 @@ -446,22 +507,6 @@
1.337
1.338 return self.encode_content(value)
1.339
1.340 - def fold(self, text):
1.341 -
1.342 - "Fold the given 'text'."
1.343 -
1.344 - line_length = self.line_length
1.345 - i = 0
1.346 - lines = []
1.347 -
1.348 - line = text[i:i+line_length]
1.349 - while line:
1.350 - lines.append(line)
1.351 - i += line_length
1.352 - line = text[i:i+line_length]
1.353 -
1.354 - return lines
1.355 -
1.356 # Public functions.
1.357
1.358 def parse(f, non_standard_newline=0, parser_cls=None):
1.359 @@ -502,4 +547,9 @@
1.360 parser = (parser_cls or StreamParser)(reader)
1.361 return iter(parser)
1.362
1.363 +def iterwrite(f, line_length=None, writer_cls=None):
1.364 + _writer = Writer(f, line_length)
1.365 + writer = (writer_cls or StreamWriter)(_writer)
1.366 + return writer
1.367 +
1.368 # vim: tabstop=4 expandtab shiftwidth=4