###################################################################### # diff experiments # # todo # separate out html vs. email diff methods from string import join, split, atoi from DocumentTemplate.DT_Util import html_quote from struct import pack, unpack from OFS.History import historicalRevision import TextFormatter from OFS import ndiff # Tim Peters' ndiff is now python 2.1's difflib module # use the former for compatibility with older zopes import re def ISJUNK(line, pat=re.compile(r"\s*$").match): return pat(line) is not None ########################################################################### # CLASS ZwikiDiffMixin # RESPONSIBILITIES # - encapsulate ZWikiPage diff functionality in a separate file # - generate human-readable diffs between page versions # COLLABORATORS ZWikiPage ########################################################################### class ZWikiDiffMixin: """ This mix-in class adds some diff methods to ZWikiPage. """ ###################################################################### # METHOD CATEGORY: diff ###################################################################### def lasttext(self, versionsBack=1): """return text of the last or an earlier revision""" versionsBack = int(versionsBack) try: # problem here - manage_change_history only gets the last # 20 revisions lastrevision = self.manage_change_history()[versionsBack] key = lastrevision['key'] serial=apply(pack, ('>HHHH',)+tuple(map(atoi, split(key,'.')))) lastself=historicalRevision(self, serial) return lastself.text() except: # return '' if we don't have a version that old return '' def textDiff(self,revA=1,revB=0,a=None,b=None): """ generate a plain text diff, optimized for human readability, between two revisions of this page, numbering back from the latest. Alternately, a and/or b texts can be specified. """ revA, revB = int(revA), int(revB) a = a or self.lasttext(versionsBack=revA) b = b or self.lasttext(versionsBack=revB) # wrap (but don't fill or pad) long lines formatter = TextFormatter.TextFormatter(( {'width':70, 'fill':0, 'pad':0}, )) a=formatter.compose((a,)) b=formatter.compose((b,)) a = split(a,'\n') b = split(b,'\n') cruncher=ndiff.SequenceMatcher( #isjunk=split, isjunk=ISJUNK, #isjunk=lambda x: x in " \\t", # requires newer difflib a=a, b=b) r = [] for tag, alo, ahi, blo, bhi in cruncher.get_opcodes(): if tag == 'replace': r.append('??changed:') r = r + self._abbreviateDiffLines(a[alo:ahi],'-',10) r = r + self._abbreviateDiffLines(b[blo:bhi],'',50) r.append('') elif tag == 'delete': r.append('--removed:') r = r + self._abbreviateDiffLines(a[alo:ahi],'-',10) r.append('') elif tag == 'insert': r.append('++added:') r = r + self._abbreviateDiffLines(b[blo:bhi],'',50) r.append('') elif tag == 'equal': pass else: raise ValueError, 'unknown tag ' + `tag` return join(r, '\n')+'\n' def _abbreviateDiffLines(self,lines,prefix,maxlines=5): output = [] if maxlines and len(lines) > maxlines: extra = len(lines) - maxlines for i in xrange(maxlines - 1): output.append(prefix + lines[i]) output.append(prefix + "[%d more line%s...]" % (extra, ((extra == 1) and '') or 's')) # not working else: for line in lines: output.append(prefix + line) return output def diff(self,revA=1,revB=0,showSteps=0): """ display a human-readable diff, formatted for web display, between two revisions of this page, numbering back from the latest. Also display some navigation links. """ revA, revB = int(revA), int(revB) #if revA < revB: # revA, revB = revB, revA #if showSteps and (revA-revB) != 1: # for r in range(revA,revB): # t = t + self.diff(r,r-1) # return t t = self.textDiff(revA=revA,revB=revB) t = html_quote(t) # careful, don't feed the regexps.. t = re.sub(r'(?s)(\?\?changed.*?)\n((-.*?\n)+?)((-\[[0-9]+ more.*?\]\n)?)([^-].*?)((\[[0-9]+ more.*?\]\n)?)(?=()?\+\+added|()?--removed|()?\?\?changed|$)', r'\1\n\2\4\6\7', t) t = re.sub(r'(?s)(\+\+added.*?\n)(.*?)((\[[0-9]+ more.*?\]\n)?)(?=()?\+\+added|()?--removed|()?\?\?changed|$)', r'\1\2\3', t) # -[43 more lines...] should not get matched in \2 t = re.sub(r'(?s)(\-\-removed.*?\n)((-.*?\n)+?)((-\[[0-9]+ more.*?\]\n)?)(?=()?\+\+added|()?--removed|()?\?\?changed|$)', r'\1\2\4', t) # move this out to dtml/* or something return """\
%s
""" % (self.page_url(), (revA>=19 and '<< previous edit') # we only see 20 revs right now or '<< previous edit' %( self.page_url(), revA+1, revA), (revB==0 and 'next edit >>' % (self.page_url())) or 'next edit >>' % ( self.page_url(), revB, max(revB-1,0)), self.page_url(), t) def oldDiff(self,revA=1,revB=0): """ display a zope-page-history-style html-formatted diff between two revisions of this page, numbering back from the latest. """ revA, revB = int(revA), int(revB) a=split(self.lasttext(versionsBack=revA),'\n') b=split(self.lasttext(versionsBack=revB),'\n') cruncher=ndiff.SequenceMatcher(isjunk=split, a=a, b=b) r = [''] for tag, alo, ahi, blo, bhi in cruncher.get_opcodes(): if tag == 'replace': replace(a, alo, ahi, b, blo, bhi, r) elif tag == 'delete': dump('-', a, alo, ahi, r) elif tag == 'insert': dump('+', b, blo, bhi, r) elif tag == 'equal': pass #dump(' ', a, alo, ahi, r) else: raise ValueError, 'unknown tag ' + `tag` r.append('
') diff = join(r, '\n') return '\n\n'+diff+'\n\n' ###################################################################### # FUNCTION CATEGORY: diff helper functions ###################################################################### def dump(tag, x, lo, hi, r): r1=[] r2=[] for i in xrange(lo, hi): r1.append(tag) r2.append(x[i]) r.append("\n" "
\n%s\n
\n" "
\n%s\n
\n" "\n" % (join(r1,'\n'), html_quote(join(r2, '\n')))) def replace(x, xlo, xhi, y, ylo, yhi, r): rx1=[] rx2=[] for i in xrange(xlo, xhi): rx1.append('-') rx2.append(x[i]) ry1=[] ry2=[] for i in xrange(ylo, yhi): ry1.append('+') ry2.append(y[i]) r.append("\n" "
\n%s\n%s\n
\n" "
\n%s\n%s\n
\n" "\n" % (join(rx1, '\n'), join(ry1, '\n'), html_quote(join(rx2, '\n')), html_quote(join(ry2, '\n'))))