[Zope3-checkins] CVS: Products3/z3checkins - message_plain.pt:1.1 configure.zcml:1.2 interfaces.py:1.2 message.pt:1.2 message.py:1.3

Marius Gedminas mgedmin@codeworks.lt
Thu, 3 Apr 2003 07:08:55 -0500


Update of /cvs-repository/Products3/z3checkins
In directory cvs.zope.org:/tmp/cvs-serv24870/z3checkins

Modified Files:
	configure.zcml interfaces.py message.pt message.py 
Added Files:
	message_plain.pt 
Log Message:
- Plain text view for checkin messages (called 'index.txt')
- Next/Previous/First/Last navigation via <link> elements
- Tabs and trailing whitespace are highlighted in diffs
- Message parsing uses less regexes (old way triggered stack overflows)



=== Added File Products3/z3checkins/message_plain.pt ===
From: <span tal:replace="structure context/author" />
Date: <span tal:replace="structure context/date/@@rfc822" />
Subject: <span tal:replace="structure string:[Zope3-checkins] CVS: ${context/subject}" />
Message-Id: <span tal:replace="structure context/message_id" />

<div tal:replace="structure context/body" />


=== Products3/z3checkins/configure.zcml 1.1.1.1 => 1.2 ===
--- Products3/z3checkins/configure.zcml:1.1.1.1	Thu Apr  3 03:19:58 2003
+++ Products3/z3checkins/configure.zcml	Thu Apr  3 07:08:24 2003
@@ -15,6 +15,11 @@
 
 <!-- Utilities -->
 
+  <adapter for="zope.app.interfaces.container.IReadContainer"
+           factory=".message.CheckinMessageAdapter"
+           permission="zope.View"
+           provides=".interfaces.ICheckinMessageArchive" />
+
   <utility factory=".message.CheckinMessageParser"
            permission="zope.View"
            provides=".interfaces.ICheckinMessageParser" />
@@ -67,6 +72,13 @@
     for=".interfaces.ICheckinMessage"
     name="index.html"
     template="message.pt"
+    class=".message.CheckinMessageView"
+    permission="zope.View" />
+
+  <browser:page
+    for=".interfaces.ICheckinMessage"
+    name="index.txt"
+    template="message_plain.pt"
     class=".message.CheckinMessageView"
     permission="zope.View" />
 


=== Products3/z3checkins/interfaces.py 1.1.1.1 => 1.2 ===
--- Products3/z3checkins/interfaces.py:1.1.1.1	Thu Apr  3 03:19:59 2003
+++ Products3/z3checkins/interfaces.py	Thu Apr  3 07:08:24 2003
@@ -36,3 +36,26 @@
         checkin message.
         """
 
+class ICheckinMessageArchive(Interface):
+    """A chronologically ordered sequence of checkin messages.
+
+    Implements the Python sequence procotol.
+    """
+
+    def __len__():
+        """Returns the number of messages in the archive."""
+
+    def __getitem__(index):
+        """Returns a given message."""
+
+    def __getslice__(start, stop):
+        """Returns a range of messages."""
+
+    def __iter__():
+        """Returns an iterator."""
+
+    def index(message):
+        """Returns the index of a given message.
+
+        Raises ValueError if message is not in the archive."""
+


=== Products3/z3checkins/message.pt 1.1.1.1 => 1.2 ===
--- Products3/z3checkins/message.pt:1.1.1.1	Thu Apr  3 03:19:58 2003
+++ Products3/z3checkins/message.pt	Thu Apr  3 07:08:24 2003
@@ -1,6 +1,14 @@
 <html>
-<head>
+<head tal:define="next view/next; prev view/previous; first view/first; last view/last">
   <title tal:content="string: ${context/author_name} - ${context/subject}" />
+  <link rel="next" tal:condition="next"
+        tal:attributes="href next/@@absolute_url" />
+  <link rel="previous" tal:condition="prev"
+        tal:attributes="href prev/@@absolute_url" />
+  <link rel="first" tal:condition="first"
+        tal:attributes="href first/@@absolute_url" />
+  <link rel="last" tal:condition="last"
+        tal:attributes="href last/@@absolute_url" />
   <style type="text/css">
     body { color: black; background: white; }
     .header { font-weight: bold; }
@@ -16,10 +24,11 @@
     .old { background: #e3e0ff; color: red; }
     .new { background: #e0ffe6; color: green; }
     .signature { color: gray; }
+    .trail { color: gray; }
+    .tab { color: gray; }
   </style>
 </head>
 <body>
-<h1 tal:content="string: ${context/author_name} - ${context/subject}" />
 <pre class="headers">
 <span class="header">From:</span> <span class="value" tal:content="context/author" />
 <span class="header">Date:</span> <span class="value" tal:content="context/date/@@rfc822" />


=== Products3/z3checkins/message.py 1.2 => 1.3 ===
--- Products3/z3checkins/message.py:1.2	Thu Apr  3 04:17:46 2003
+++ Products3/z3checkins/message.py	Thu Apr  3 07:08:24 2003
@@ -11,12 +11,16 @@
 from StringIO import StringIO
 from datetime import datetime, tzinfo, timedelta
 from persistence import Persistent
-from zope.component import getUtility
+from zope.component import getUtility, getAdapter, queryAdapter
 from zope.app.browser.form.widget import FileWidget
 from zope.app.form.widget import CustomWidget
 from zope.proxy.context import ContextWrapper
+from zope.app.interfaces.container import IReadContainer
+from zope.proxy.introspection import removeAllProxies
+from zope.proxy.context import getWrapperContainer
 
 from interfaces import ICheckinMessage, ICheckinMessageParser, FormatError
+from interfaces import ICheckinMessageArchive
 
 __metaclass__ = type
 
@@ -165,6 +169,39 @@
         return "".join(log_message).strip()
 
 
+class CheckinMessageAdapter:
+    """Adapts a container to a checkin message archive."""
+
+    __implements__ = ICheckinMessageArchive
+    __used_for__ = IReadContainer
+
+    def __init__(self, context):
+        self.context = context
+        items = []
+        for key, item in self.context.items():
+            if ICheckinMessage.isImplementedBy(item):
+                items.append((item.date, key, item))
+        items.sort()
+        self.messages = [ContextWrapper(item, self.context, name=key)
+                             for date, key, item in items]
+
+    def __len__(self):
+        return len(self.messages)
+
+    def __getitem__(self, index):
+        return self.messages[index]
+
+    def __getslice__(self, start, stop):
+        return self.messages[start:stop]
+
+    def __iter__(self):
+        return iter(self.messages)
+
+    def index(self, message):
+        return self.messages.index(message)
+
+
+
 #
 # Browser views
 #
@@ -190,14 +227,12 @@
         """Returns a list of the last 'how_many' checkin messages in
         self.context, newest first.
         """
-        items = []
-        for key, item in self.context.items():
-            if ICheckinMessage.isImplementedBy(item):
-                items.append((item.date, key, item))
-        items.sort()
+        if not hasattr(self, '_archive'):
+            self._archive = getAdapter(self.context, ICheckinMessageArchive)
+        items = self._archive[-how_many:]
+        items = removeAllProxies(items) # needed for reverse
         items.reverse()
-        return [ContextWrapper(item, self.context, name=key)
-                    for date, key, item in items[:how_many]]
+        return items
 
 
 class CheckinMessageView:
@@ -205,6 +240,48 @@
 
     Provides a syntax-highlighted body."""
 
+    def _calc_index(self):
+        if not hasattr(self, '_archive'):
+            container = getWrapperContainer(self.context)
+            self._archive = container and queryAdapter(container,
+                                                       ICheckinMessageArchive)
+        if not self._archive:
+            self._index = None
+        elif not hasattr(self, '_index'):
+            self._index = self._archive.index(self.context)
+
+    def next(self):
+        """Returns the next message in archive."""
+        self._calc_index()
+        if self._index is not None and self._index < len(self._archive) - 1:
+            return self._archive[self._index + 1]
+        else:
+            return None
+
+    def previous(self):
+        """Returns the previous message in archive."""
+        self._calc_index()
+        if self._index is not None and self._index > 0:
+            return self._archive[self._index - 1]
+        else:
+            return None
+
+    def first(self):
+        """Returns the first message in archive."""
+        self._calc_index()
+        if self._archive:
+            return self._archive[0]
+        else:
+            return None
+
+    def last(self):
+        """Returns the last message in archive."""
+        self._calc_index()
+        if self._archive:
+            return self._archive[-1]
+        else:
+            return None
+
     def body(self):
         """Colorizes checkin message body."""
 
@@ -213,24 +290,48 @@
                                 .replace('<', '&lt;') \
                                 .replace('>', '&gt;') \
                                 .replace('"', '&quot;')
-        m = re.match('(?s)(.*?\nLog [mM]essage:\n)(.*?\n)\n*(Status:\n\n.*?)?'
-                     '(===.*?)?'
-                     '(\n_______________________________________________.*)?\Z',
-                     text)
-        if not m:
+        log_idx = text.find('\nLog message:\n')
+        if log_idx == -1:
+            log_idx = text.find('\nLog Message:\n')
+        if log_idx == -1:
             return '<pre>%s</pre>' % text
-        intro, log, import_status, diff, sig = m.groups()
+        log_idx += len('\nLog message:\n')
+
+        sig_idx = text.rfind(
+                        '\n_______________________________________________')
+        if sig_idx == -1:
+            sig_idx = len(text)
+
+        diff_idx = text.find('\n===')
+        if diff_idx == -1:
+            diff_idx = sig_idx
+
+        status_idx = text.find('\nStatus:\n')
+        if status_idx == -1:
+            status_idx = diff_idx
+
+        assert log_idx <= status_idx <= diff_idx <= sig_idx
+
+        intro = text[:log_idx]
+        log = text[log_idx:status_idx].strip()
+        import_status = text[status_idx:diff_idx]
+        diff = text[diff_idx:sig_idx]
+        sig = text[sig_idx:]
 
         log = '<p>%s</p>' % '</p><p>'.join(log.splitlines())
 
         if import_status is None:
             import_status = ''
 
-        # Unified diff
         if diff is None:
             diff = ''
+
+        diff = "\n".join(map(self.mark_whitespace, diff.splitlines()))
+
         def colorize(style):
             return r'<div class="%s">\1</div>' % style
+
+        # Unified diff
         diff = re.sub(r'(?m)^(===.*)$', colorize("file"), diff)
         diff = re.sub(r'(?m)^(---.*)$', colorize("oldfile"), diff)
         diff = re.sub(r'(?m)^(\+\+\+.*)$', colorize("newfile"), diff)
@@ -241,10 +342,62 @@
         # Postprocess for Mozilla
         diff = re.sub('</div>\n', '\n</div>', diff)
 
-        if sig is None:
-            sig = ''
-        else:
+        if sig:
             sig = '<div class="signature">%s</div>' % sig
 
         return '<pre>%s</pre><div class="log">%s</div><pre>%s%s%s</pre>' \
                % (intro, log, import_status, diff, sig)
+
+    def mark_whitespace(self, line, tab=('>', '-'), trail='.'):
+        """Mark whitespace in diff lines.
+
+        Suggested values for tab: ('>', '-'), ('&#187;', ' '),
+                                  ('&#187;', '&#x2010;')
+
+        Suggested values for trail: '.', '&#x2423;'
+        """
+        if line == ' ' or (not line.endswith(' ') and '\t' not in line):
+            return line
+        m = re.search('\s+\Z', line)
+        if m:
+            n = m.start()
+            if n == 0:
+                n = 1 # don't highlight the first space in a diff
+            line = '%s<span class="trail">%s</span>' % (line[:n],
+                            line[n:].replace(' ', trail))
+        if '\t' in line:
+            NORMAL, TAG, ENTITY = 0, 1, 2
+            idx = col = 0
+            mode = NORMAL
+            tabs = []
+            for c in line[1:]: # ignore first space in a diff
+                idx += 1
+                if mode == TAG:
+                    if c == '>':
+                        mode = NORMAL
+                elif mode == ENTITY:
+                    if c == ';':
+                        col += 1
+                        mode = NORMAL
+                else:
+                    if c == '<':
+                        mode = TAG
+                    elif c == '&':
+                        mode = ENTITY
+                    elif c == '\t':
+                        width = 8 - (col % 8)
+                        tabs.append((idx, width))
+                        col += width
+                    else:
+                        col += 1
+            if tabs:
+                parts = []
+                last = 0
+                for idx, width in tabs:
+                    parts.append(line[last:idx])
+                    parts.append('<span class="tab">%s%s</span>'
+                                 % (tab[0], tab[1] * (width - 1)))
+                    last = idx + 1
+                parts.append(line[last:])
+                line = "".join(parts)
+        return line