#!/usr/bin/env python """ document.py - Emano Document Class Author: Sean B. Palmer, inamidst.com Source: http://inamidst.com/emano/ """ import os, re, md5 import schemes r_scheme = re.compile('^([^/:]+):') def getscheme(uri): match = r_scheme.match(uri) if match: scheme = match.group(1) return scheme.lower() else: return '_' class Document(object): def __init__(self, editor, uri=None, lines=None): if not (uri or lines): lines = [''] self.editor = editor self.schemes = schemes.Schemes() self.uri = uri self.default() if (not lines) and (uri is not None): self.openURI(uri) else: self.openLines(lines=lines, uri=uri) self.rehash() def default(self): # @@ Where is this being called from? self.pos = [0, 0] self.startline = 0 self.startchar = {} self.selection = [] self.error = None self.__lines = [] self.encoding = self.editor.config.DefaultInputEncoding or 'utf-8' self.linesep = '\n' def openURI(self, uri): """Open and feed it into the Document.""" # @@ Handlers for data (read) and ftp (read and write) self.default() # Strip any trailing hashes off of the URI if uri.endswith('#'): uri = uri.rstrip('#') # @@ 1 should be the default message persistence period self.editor.statwin.notice('Warning: stripped trailing #', 1) scheme = getscheme(uri) read = self.schemes.read(scheme) if read is not False: read(self, uri) else: raise ValueError("Scheme %r not supported" % scheme) if not self.error: # @@ Add optional backup facility self.uri = uri self.rehash() def openLines(self, lines, uri=None, linesep=None): self.default() self.__lines = lines if uri is not None: self.uri = uri if linesep is None: self.linesep = '\n' else: self.linesep = linesep self.rehash() def append(self, line): if self.linesep is None: if line.endswith('\r\n'): self.linesep = '\r\n' elif line.endswith('\n'): self.linesep = '\n' else: self.linesep = '' line = line.rstrip('\r\n') if self.encoding != 'bytes': try: line = line.decode(self.encoding) except UnicodeDecodeError: # d'oh, pwned byteLines = [] for oldline in self.__lines: byteLines.append(oldline.encode(self.encoding)) self.__lines = byteLines self.__lines.append(line) self.encoding = 'bytes' elif not isinstance(line, str): line = str(line) self.__lines.append(line) def save(self, uri=None): uri = self.uri or uri if (not uri) or uri.endswith('#'): if not uri: msg = 'Error: invalid filename' elif uri.endswith('#'): msg = "Error: I don't save filenames with #" self.editor.statwin.notice(msg) return scheme = getscheme(uri) write = self.schemes.write(scheme) if write is not False: message = write(self, uri) else: raise ValueError("Scheme %r not supported" % scheme) self.editor.statwin.notice(message) # Get stuff def line(self, i): return self.__lines[i] def getContent(self, lines=None): if lines is None: lines = self.__lines if (self.encoding not in (None, 'bytes')): lines = [line.encode(self.encoding) for line in lines] return self.linesep.join(lines) def length(self): return len(self.__lines) def getTitle(self): if self.uri is not None: return self.uri if not self.__lines: return 'Empty File (0 bytes)' # @@ Should base this value on screensize etc. if len(self.line(0)) > 35: first = self.line(0)[:35] + '...' else: first = self.line(0) length = len(self.getContent()) unit = ('byte', 'bytes')[length != 1] if first: return 'New File ("%s"; length: %s %s)' % (first, length, unit) return 'New File (length: %s %s)' % (length, unit) # return '[file of length: %s %s]' % (length, unit) def modified(self): """Return boolean True if document is modified; False otherwise.""" linelength, textlength, hash = self.hash newlinelength = self.length() if linelength != newlinelength: return True content = self.getContent() newtextlength = len(content) if textlength != newtextlength: return True if newtextlength > 1000000: # Don't md5sum particularly large files return True if isinstance(content, unicode): content = content.encode('utf-8') newhash = md5.new(content).hexdigest() return (hash != newhash) def getStartchar(self, line): return self.startchar.get(line) or 0 def isSelectionLine(self, y): if not self.selection: return False (topy, topx), (boty, botx) = self.getTopAndBot() if (y >= topy) and (y <= boty): return True return False def getSelection(self): if self.selection: (topy, topx), (boty, botx) = self.getTopAndBot() lines = self.__lines[topy:boty + 1] if len(lines) > 1: lines[0] = lines[0][topx:] lines[-1] = lines[-1][:botx] else: lines[0] = lines[0][topx:botx] return self.linesep.join(lines) else: return False def getTopAndBot(self): fore, aft = tuple(self.selection) # Find which of these comes first if fore < aft: top, bot = fore, aft else: top, bot = aft, fore return top, bot # Set stuff def setStartchar(self, line, equals=None, add=None): if equals is not None: self.startchar[line] = equals elif add is not None: self.startchar[line] = add else: raise ValueError("Was expecting another arg") def rehash(self): # @@ use digest rather than hexdigest? # @@ could just use an adler or crc32 checksum too since it's faster linelength = self.length() content = self.getContent() textlength = len(content) if isinstance(content, unicode): content = content.encode('utf-8') hash = md5.new(content).hexdigest() self.hash = (linelength, textlength, hash) # * * * Nice Clean New Methods * * * def insert(self, y, x, content, update=True): # @@ really, it's p and q. well, q and p # print >> debug, 'INSERT', y, x, `content` lines = content.split(self.linesep) # @@ '\n'? line = self.line(y) head, tail = line[:x], line[x:] lines[0] = head + lines[0] lines[-1] = lines[-1] + tail self.__lines[y:y+1] = lines q = y + content.count('\n') if '\n' in content: p = len(content) - (content.rfind('\n') + 1) else: p = x + len(content) return q, p def insertList(self, y, x, lines, update=True): # @@ Is this used? self.insert(y, x, self.linesep.join(lines), update=update) def delete(self, (y0, x0), (y1, x1), update=True): # @@ This assertion doesn't check properly # assert (y0 <= y1) and (x0 <= x1) # print >> debug, 'DELETE', (y0, x0), (y1, x1) if y0 == y1: # Delete part of a single line line = self.line(y0) self.__lines[y0] = line[:x0] + line[x1:] else: line = self.line(y0)[:x0] + self.line(y1)[x1:] self.__lines[y0:y1+1] = [line] return y0, x0 def undo(self): pass # @@ def redo(self): pass # @@ if __name__=="__main__": main()