--- /tmp/breadcrumbs.py	2011-11-14 12:51:12.022785922 +0100
+++ breadcrumbs.py	2011-11-14 13:00:30.330802558 +0100
@@ -1,205 +1,205 @@
-# -*- coding: utf-8 -*-
-
-import cPickle
-import re
-
-from genshi.builder import tag
-from genshi.filters.transform import Transformer
-from pkg_resources import resource_filename
-
-from trac.core import Component, TracError, implements
-from trac.config import IntOption, ListOption, Option
+# coding: utf-8
+from trac.core import Component, implements, TracError
+from trac.config import Option, IntOption, ListOption
 from trac.env import IEnvironmentSetupParticipant
+from trac.resource import Resource, resource_exists, get_resource_shortname, get_resource_summary, get_resource_url
 from trac.web import IRequestFilter
-from trac.web.api import ITemplateStreamFilter
 from trac.web.chrome import ITemplateProvider, add_stylesheet
-from trac.wiki import parse_args
+from trac.web.api import ITemplateStreamFilter
+from trac.util.text import to_unicode
+from genshi.core import QName, Attrs
+from genshi.builder import tag
+from genshi.filters.transform import Transformer
+from pkg_resources import resource_filename
+import re
+import cPickle
+import os
 
 
 class BreadCrumbsSystem(Component):
-    """Provider of bread cumbs navigation bar right below Trac metanav."""
-
-    implements(IEnvironmentSetupParticipant, IRequestFilter,
-               ITemplateProvider, ITemplateStreamFilter)
+    implements(IRequestFilter, ITemplateProvider, ITemplateStreamFilter, IEnvironmentSetupParticipant)
 
-    ignore_pattern = Option('breadcrumbs', 'ignore_pattern', None,
+    ignore_pattern = Option('breadcrumbs', 'ignore pattern', None,
         doc="""Resource names that match this pattern will not be added to
-            the breadcrumbs trail.""")
-
-    label = Option('breadcrumbs', 'label', '',
-        doc="""Text label to show before breadcrumb list. If empty,
-            'Breadcrumbs:' is used as default.""")
+        the breadcrumbs trail.""")
 
     max_crumbs = IntOption('breadcrumbs', 'max_crumbs', 6,
-        doc="""Indicates maximum number of breadcrumbs to store per user.""")
-
-    supported_paths = ListOption('breadcrumbs', 'paths',
-        '/wiki/,/ticket/,/milestone/',
-        doc="""List of URL paths to allow breadcrumb tracking.
-            Globs are supported.""")
+        doc="""Indicates the maximum number of breadcrumbs to store per user.""")
+    supported_realms = ListOption('breadcrumbs', 'realms', 'wiki, ticket, milestone, pastebin, report, changeset, source')
 
+    label = Option('breadcrumbs','label','Breadcrumbs:',
+                   doc="""Text label to show before breadcrumb list""")
+    max_link_length = IntOption('breadcrumbs', 'max_link_length', 30,
+                                doc="Link max length (0 = unlimited)")
     compiled_ignore_pattern = None
 
-    # IEnvironmentSetupParticipant methods
+    ignore_resources = ListOption('breadcrumbs', 'ignore_resources', 'wiki:WikiStart',
+                                  doc="These resources won't show up in breadcrumb list.")
+    
+    name_map = ListOption('breadcrumbs', 'name_map', 'source:/, Files', doc="Change displayed names")
+    
+    ## IEnvironmentSetupParticipant
     def environment_created(self):
-        self._upgrade_db(self.env.get_db_cnx())
+        self._upgrade_db()
 
-    def environment_needs_upgrade(self, db):
+    def environment_needs_upgrade(self, db=None):
+        if not db:
+            db = self.env.get_read_db()
         cursor = db.cursor()
-
         try:
-            cursor.execute("""
-                SELECT count(*)
-                  FROM session_attribute
-                 WHERE name = %s
-                """, ("breadcrumbs list",)
+            cursor.execute(
+                "SELECT count(*) FROM session_attribute WHERE name = %s",
+                ("breadcrumbs list",)
             )
             result = cursor.fetchone()
             if int(result[0]):
                 return True
-
             return False
         except:
-            db.rollback()
             return True
 
-    def upgrade_environment(self, db):
+    def upgrade_environment(self, db=None):
         self._upgrade_db(db)
 
-    def _upgrade_db(self, db):
+    def _upgrade_db(self, db=None):
         try:
-            from trac.db import DatabaseManager
-            db_backend, _ = DatabaseManager(self.env)._get_connector()
-
-            cursor = db.cursor()
-            cursor.execute("""
-                DELETE
-                  FROM session_attribute
-                 WHERE name = %s
-                """, ("breadcrumbs list",)
-            )
+            @self.env.with_transaction(db)
+            def do_upgrade(db):
+                cursor = db.cursor()
+                cursor.execute("DELETE FROM session_attribute WHERE name = %s", ("breadcrumbs list",))
         except Exception, e:
             db.rollback()
             self.log.error(e, exc_info=True)
             raise TracError(str(e))
 
-    # IRequestFilter methods
+    ## IRequestFilter
+
     def pre_process_request(self, req, handler):
         return handler
+
+    def _get_crumbs(self, sess):
+        crumbs = []
+        if 'breadcrumbs' in sess:
+            # See _store_crumbs
+            try:
+                raw = sess['breadcrumbs'].decode('base64')
+                dicts = cPickle.loads(raw)
+                for d in dicts:
+                    crumbs.append(self._dict_to_resource(d))
+            except:
+                self.log.exception('Failed to unpickle breadcrumbs')
+                del sess['breadcrumbs']
+        return crumbs
+
+    def _store_crumbs(self, session, crumbs):
+        pickled = cPickle.dumps([self._resource_to_dict(c) for c in crumbs])
+        # pickle needs str while trac enforces unicode on session attributes, so let's
+        # base64-encode the pickled data.
+        session['breadcrumbs'] = pickled.encode('base64')
         
+    def _dict_to_resource(self, rdict):
+        resource = Resource()
+        for k in resource.__slots__:
+            setattr(resource, k, rdict[k])
+        if rdict.get('parent'):
+            resource.parent = self._dict_to_resource(rdict['parent'])
+        return resource
+
+    def _resource_to_dict(self, resource):
+        rdict = dict([(k, getattr(resource, k)) for k in resource.__slots__])
+        if resource.parent:
+            rdict.update(parent=self._resource_to_dict(resource.parent))
+        return rdict
+
     def post_process_request(self, req, template, data, content_type):
+        def _break():
+            return template, data, content_type
+        if req.authname == 'anonymous':
+            return _break()
+        # Don't ever breadcrumb ajax requests
+        if data and data.get('xhr'):
+            return _break()
+        resource = None
+        try:
+            resource = data['context'].resource
+        except:
+            self.log.debug('No context/resource found, path=%s', req.path_info)
+            return _break()
+        if not resource or resource.realm not in self.supported_realms:
+            return _break()
+        for r in self.ignore_resources:
+            if "%s:%s" % (resource.realm, resource.id) == r:
+                return _break()
         if self.compiled_ignore_pattern is None and self.ignore_pattern:
             self.compiled_ignore_pattern = re.compile(self.ignore_pattern)
-
-        path = req.path_info
+        if (self.compiled_ignore_pattern and
+                            self.compiled_ignore_pattern.match(to_unicode(resource.id))):
+            return _break()
         try:
-            if path.count('/') >= 2:
-                _, realm, resource = path.split('/', 2)
-
-                supported = False
-
-                for pattern in self.supported_paths:
-                    if re.match(pattern, path):
-                        supported = True
-                        break
-
-                # Prevent tracking of prefetched pages as reported for
-                #   Mozilla browsers.
-                if req.get_header("X-Moz") == "prefetch":
-                    supported = False
-
-                if not supported or (self.compiled_ignore_pattern and
-                            self.compiled_ignore_pattern.match(resource)):
-                    return template, data, content_type
-
-                if '&' in resource:
-                    resource = resource[0:resource.index('&')]
-
-                sess = req.session
-                crumbs = self._get_crumbs(sess)
-                
-                current = '/'.join( (realm, resource) )
-                if current in crumbs:
-                    crumbs.remove(current)
-                    crumbs.insert(0, current)
-                else:
-                    crumbs.insert(0, current)
-                    # Keep one over max for providing max length even
-                    # when hiding current in first position while viewing it.
-                    crumbs = crumbs[0:self.max_crumbs + 1]
-
-                sess['breadcrumbs_list'] = cPickle.dumps(crumbs)
+            # We might have an unfinalized resource (/newticket will have a ticket
+            # resource without id in the context for instance)
+            assert resource_exists(self.env, resource)
         except:
-            self.log.exception("Breadcrumb failed :(")
-
+            return _break()
+        crumbs = self._get_crumbs(req.session)
+        resource.version = None
+        if resource in crumbs:
+            crumbs.remove(resource)
+        crumbs.insert(0, resource)
+        self._store_crumbs(req.session, crumbs[:self.max_crumbs])
         return template, data, content_type
 
-    def _get_crumbs(self, sess):
-        crumbs = []
-        if 'breadcrumbs_list' in sess:
-            raw = sess['breadcrumbs_list']
-            try:
-                crumbs = cPickle.loads(raw.encode('ascii', 'ignore'))
-            except:
-                del sess['breadcrumbs_list']
-
-        return crumbs
+    ## ITemplateProvider
 
-    # ITemplateProvider methods
     def get_htdocs_dirs(self):
-        from pkg_resources import resource_filename
+        
         return [('breadcrumbs', resource_filename(__name__, 'htdocs'))]
 
     def get_templates_dirs(self):
         return []
 
-    # ITemplateStreamFilter method
+    ## ITemplateStreamFilter
+
     def filter_stream(self, req, method, filename, stream, data):
         crumbs = self._get_crumbs(req.session)
         if not crumbs:
             return stream
-
         add_stylesheet(req, 'breadcrumbs/css/breadcrumbs.css')
-        ul = []
-
-        path = req.path_info
-        if path.count('/') >= 2:
-            _, realm, resource = path.split('/', 2)
-            if '&' in resource:
-                resource = resource[0:resource.index('&')]
-            current = '/'.join( (realm, resource) )
-        else:
-            current = None
-
-        href = req.href(req.base_path)
-        offset = 0
-        if crumbs and crumbs[0] == current:
-            offset = 1
-        for crumb in crumbs[offset:self.max_crumbs + offset]:
-            realm, resource = crumb.split('/', 1)
-            name = resource.replace('_', ' ')
-
-            if realm == "ticket":
-                name = "#" + resource
-            elif realm != "wiki":
-                name = "%s:%s" % (realm, name)
-
-            link = req.href(realm, resource)
-
-            first = ul == []
-            li = tag.li(tag.a(title=name, href=link)(name))
-            if first:
-                li(class_="first")
-            ul.append(li)
-
-        if ul:
-            last = ul.pop()
-            ul.append(last(class_="last"))
-            insert = tag.ul(class_="nav", id="breadcrumbs"
-                     )(tag.li(self.label and self.label or \
-                              "Breadcrumbs:"), ul)
+        li_items = []
+        badcrumbs = [] # eg stuff that's been deleted after they were added to crumbs
+        for r in crumbs:
+            try:
+                name = self._shorten(r.realm, get_resource_shortname(self.env, r))
+                title = get_resource_summary(self.env, r)
+                link = get_resource_url(self.env, r, req.href)
+                li_items.append(tag.li(tag.a(name, title=title, href=link)))
+            except:
+                badcrumbs.append(r)
+        if badcrumbs:
+            for r in badcrumbs:
+                crumbs.remove(r)
+            self._store_crumbs(req.session, crumbs)
+        if len(li_items) == 1:
+            li_items[-1].attrib = Attrs([(QName('class'),'first last')])
         else:
-            insert = ''
-
-        return stream | Transformer('//div[@id="metanav"]/ul').after(insert)
-
+            if li_items:
+                li_items[0].attrib = Attrs([(QName('class'),'first')])
+            if len(li_items) > 1:
+                li_items[-1].attrib = Attrs([(QName('class'),'last')])
+        ul = tag.ul(class_="nav", id="breadcrumbs")(tag.li(self.label, class_="heading"), li_items)
+        return stream | Transformer('//div[@id="breadcrumbsbox"]').append(ul)
+
+    def _shorten(self, realm, text):
+        for old, new in zip(self.name_map, self.name_map[1:])[::2]:
+            if "%s:%s" % (realm, text) == old:
+                text = new
+        if self.max_link_length == 0 or len(text) <= self.max_link_length:
+            return text
+        if realm == 'source':
+            if text.count(os.sep) > 1:
+                text = os.sep.join([text[0:text.find(os.sep)], '...',
+                                    os.path.basename(text)])
+        elif realm == 'wiki':
+            if text.count('/') >= 1:
+                text = text.rsplit('/', 1)[1]
+        return text
