Index: wiki-default/checkwiki.py
===================================================================
--- wiki-default/checkwiki.py	(revision 2797)
+++ wiki-default/checkwiki.py	(working copy)
@@ -15,6 +15,9 @@
 # Pages to include in distribution
 wiki_pages = [
  "CamelCase",
+ "InterMapTxt",
+ "InterTrac",
+ "InterWiki",
  "RecentChanges",
  "TitleIndex",
  "TracAccessibility",
Index: trac/env.py
===================================================================
--- trac/env.py	(revision 2797)
+++ trac/env.py	(working copy)
@@ -74,6 +74,7 @@
         ComponentManager.__init__(self)
 
         self.path = path
+        self.siblings = {}
         self.__cnx_pool = None
         if create:
             self.create(db_str)
Index: trac/ticket/api.py
===================================================================
--- trac/ticket/api.py	(revision 2797)
+++ trac/ticket/api.py	(working copy)
@@ -19,7 +19,7 @@
 from trac import util
 from trac.core import *
 from trac.perm import IPermissionRequestor
-from trac.wiki import IWikiSyntaxProvider
+from trac.wiki import IWikiSyntaxProvider, Formatter
 from trac.Search import ISearchSource, query_to_sql, shorten_result
 
 
@@ -140,10 +140,18 @@
                 ('ticket', self._format_link)]
 
     def get_wiki_syntax(self):
-        yield (r"!?(?<!&)#\d+", # #123 but not &#123; (HTML entity)
-               lambda x, y, z: self._format_link(x, 'ticket', y[1:], y))
+        yield (
+            # matches #... but not &#... (HTML entity)
+            r"!?(?<!&)#"
+            # optional intertrac shorthand #T... + digits
+            r"(?P<it_ticket>%s)?\d+" % Formatter.INTERTRAC_SCHEME,
+            lambda x, y, z: self._format_link(x, 'ticket', y[1:], y, z))
 
-    def _format_link(self, formatter, ns, target, label):
+    def _format_link(self, formatter, ns, target, label, fullmatch=None):
+        intertrac = formatter.shorthand_intertrac_helper(ns, target, label,
+                                                         fullmatch)
+        if intertrac:
+            return intertrac
         cursor = formatter.db.cursor()
         cursor.execute("SELECT summary,status FROM ticket WHERE id=%s",
                        (target,))
Index: trac/ticket/report.py
===================================================================
--- trac/ticket/report.py	(revision 2829)
+++ trac/ticket/report.py	(working copy)
@@ -25,7 +25,7 @@
 from trac.perm import IPermissionRequestor
 from trac.web import IRequestHandler
 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
-from trac.wiki import wiki_to_html, IWikiSyntaxProvider
+from trac.wiki import wiki_to_html, IWikiSyntaxProvider, Formatter
 
 
 dynvars_re = re.compile('\$([A-Z]+)')
@@ -501,9 +501,14 @@
         yield ('report', self._format_link)
 
     def get_wiki_syntax(self):
-        yield (r"!?\{\d+\}", lambda x, y, z: self._format_link(x, 'report', y[1:-1], y))
+        yield (r"!?\{(?P<it_report>%s\s*)?\d+\}" % Formatter.INTERTRAC_SCHEME,
+               lambda x, y, z: self._format_link(x, 'report', y[1:-1], y, z))
 
-    def _format_link(self, formatter, ns, target, label):
+    def _format_link(self, formatter, ns, target, label, fullmatch=None):
+        intertrac = formatter.shorthand_intertrac_helper(ns, target, label,
+                                                         fullmatch)
+        if intertrac:
+            return intertrac
         report, args = target, ''
         if '?' in target:
             report, args = target.split('?')
Index: trac/versioncontrol/web_ui/changeset.py
===================================================================
--- trac/versioncontrol/web_ui/changeset.py	(revision 2797)
+++ trac/versioncontrol/web_ui/changeset.py	(working copy)
@@ -30,7 +30,8 @@
 from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff
 from trac.web import IRequestHandler
 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
-from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider
+from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider, \
+                      Formatter
 
 
 class ChangesetModule(Component):
@@ -353,15 +354,26 @@
     # IWikiSyntaxProvider methods
     
     def get_wiki_syntax(self):
-        yield (r"!?\[\d+\]|(?:\b|!)r\d+\b(?!:\d)",
-               lambda x, y, z: self._format_link(x, 'changeset',
-                                                 y[0] == 'r' and y[1:]
-                                                 or y[1:-1], y))
+        yield (
+            # [...] form: start with optional intertrac: [T... or [trac ... 
+            r"!?\[(?P<it_changeset>%s\s*)?" % Formatter.INTERTRAC_SCHEME +
+            #  digits
+            r"\d+\]|"   
+            # r... form: allow r1 but not r1:2 (handled by the log syntax)
+            r"(?:\b|!)r\d+\b(?!:\d)",
+            lambda x, y, z:
+            self._format_link(x, 'changeset',
+                              y[0] == 'r' and y[1:] or y[1:-1],
+                              y, z))
 
     def get_link_resolvers(self):
         yield ('changeset', self._format_link)
 
-    def _format_link(self, formatter, ns, rev, label):
+    def _format_link(self, formatter, ns, rev, label, fullmatch=None):
+        intertrac = formatter.shorthand_intertrac_helper(ns, rev, label,
+                                                         fullmatch)
+        if intertrac:
+            return intertrac
         cursor = formatter.db.cursor()
         cursor.execute('SELECT message FROM revision WHERE rev=%s', (rev,))
         row = cursor.fetchone()
Index: trac/wiki/api.py
===================================================================
--- trac/wiki/api.py	(revision 2797)
+++ trac/wiki/api.py	(working copy)
@@ -187,7 +187,7 @@
     def get_wiki_syntax(self):
         ignore_missing = self.config.getbool('wiki', 'ignore_missing_pages')
         yield (r"!?(?<!/)\b[A-Z][a-z]+(?:[A-Z][a-z]*[a-z/])+"
-                "(?:#[A-Za-z0-9]+)?(?=\Z|\s|[.,;:!?\)}\]])",
+                "(?:#[A-Za-z0-9]+)?(?=:?\Z|:?\s|[.,;!?\)}\]])",
                lambda x, y, z: self._format_link(x, 'wiki', y, y,
                                                  ignore_missing))
 
Index: trac/wiki/tests/wiki-tests.txt
===================================================================
--- trac/wiki/tests/wiki-tests.txt	(revision 2797)
+++ trac/wiki/tests/wiki-tests.txt	(working copy)
@@ -148,10 +148,10 @@
 </p>
 ------------------------------
 ==============================
-CamelCase,CamelCase.CamelCase:CamelCase
+CamelCase,CamelCase.CamelCase: CamelCase
 ------------------------------
 <p>
-<a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a>,<a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a>.CamelCase:CamelCase
+<a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a>,<a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a>.<a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a>: <a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a>
 </p>
 ------------------------------
 ==============================
@@ -872,3 +872,78 @@
 ------------------------------
 || a || 
 || b ||
+==============================
+t:wiki:InterTrac
+trac:wiki:InterTrac
+[t:wiki:InterTrac intertrac]
+[trac:wiki:InterTrac intertrac]
+------------------------------
+<p>
+<a class="ext-link" href="http://projects.edgewall.com/trac/wiki/InterTrac" title="wiki:InterTrac in Trac's Trac"><span class="icon"></span>t:wiki:InterTrac</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/wiki/InterTrac" title="wiki:InterTrac in Trac's Trac"><span class="icon"></span>trac:wiki:InterTrac</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/wiki/InterTrac" title="wiki:InterTrac in Trac's Trac"><span class="icon"></span>intertrac</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/wiki/InterTrac" title="wiki:InterTrac in Trac's Trac"><span class="icon"></span>intertrac</a>
+</p>
+------------------------------
+==============================
+trac:ticket:2041
+[trac:ticket:2041 Trac #2041]
+#T2041
+#trac2041
+------------------------------
+<p>
+<a class="ext-link" href="http://projects.edgewall.com/trac/ticket/2041" title="ticket:2041 in Trac's Trac"><span class="icon"></span>trac:ticket:2041</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/ticket/2041" title="ticket:2041 in Trac's Trac"><span class="icon"></span>Trac #2041</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/ticket/2041" title="ticket:2041 in Trac's Trac"><span class="icon"></span>#T2041</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/ticket/2041" title="ticket:2041 in Trac's Trac"><span class="icon"></span>#trac2041</a>
+</p>
+------------------------------
+==============================
+trac:changeset:2081
+[trac:changeset:2081 Trac r2081]
+[T2081]
+[trac2081]
+[trac 2081]
+------------------------------
+<p>
+<a class="ext-link" href="http://projects.edgewall.com/trac/changeset/2081" title="changeset:2081 in Trac's Trac"><span class="icon"></span>trac:changeset:2081</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/changeset/2081" title="changeset:2081 in Trac's Trac"><span class="icon"></span>Trac r2081</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/changeset/2081" title="changeset:2081 in Trac's Trac"><span class="icon"></span>[T2081]</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/changeset/2081" title="changeset:2081 in Trac's Trac"><span class="icon"></span>[trac2081]</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/changeset/2081" title="changeset:2081 in Trac's Trac"><span class="icon"></span>[trac 2081]</a>
+</p>
+------------------------------
+==============================
+trac:report:1
+[trac:report:1 Trac r1]
+{T1}
+{trac1}
+{trac 1}
+------------------------------
+<p>
+<a class="ext-link" href="http://projects.edgewall.com/trac/report/1" title="report:1 in Trac's Trac"><span class="icon"></span>trac:report:1</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/report/1" title="report:1 in Trac's Trac"><span class="icon"></span>Trac r1</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/report/1" title="report:1 in Trac's Trac"><span class="icon"></span>{T1}</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/report/1" title="report:1 in Trac's Trac"><span class="icon"></span>{trac1}</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/report/1" title="report:1 in Trac's Trac"><span class="icon"></span>{trac 1}</a>
+</p>
+------------------------------
+==============================
+t:InterTrac
+trac:InterTrac
+[t:InterTrac intertrac]
+[trac:InterTrac intertrac]
+T:r2081
+T:#2041
+trac:#2041
+------------------------------
+<p>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=InterTrac" title="InterTrac in Trac's Trac"><span class="icon"></span>t:InterTrac</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=InterTrac" title="InterTrac in Trac's Trac"><span class="icon"></span>trac:InterTrac</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=InterTrac" title="InterTrac in Trac's Trac"><span class="icon"></span>intertrac</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=InterTrac" title="InterTrac in Trac's Trac"><span class="icon"></span>intertrac</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=r2081" title="r2081 in Trac's Trac"><span class="icon"></span>T:r2081</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=%232041" title="#2041 in Trac's Trac"><span class="icon"></span>T:#2041</a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=%232041" title="#2041 in Trac's Trac"><span class="icon"></span>trac:#2041</a>
+</p>
+------------------------------
Index: trac/wiki/tests/formatter.py
===================================================================
--- trac/wiki/tests/formatter.py	(revision 2797)
+++ trac/wiki/tests/formatter.py	(working copy)
@@ -1,116 +1,121 @@
-from __future__ import generators
-import os
-import inspect
-import StringIO
-import unittest
-
-from trac.core import *
-from trac.wiki.formatter import Formatter, OneLinerFormatter
-from trac.wiki.api import IWikiMacroProvider
-
-
-class DummyHelloWorldMacro(Component):
-    """
-    A dummy macro used by the unit test. We need to supply our own macro
-    because the real HelloWorld-macro can not be loaded using our
-    'fake' environment.
-    """
-    implements(IWikiMacroProvider)
-
-    def get_macros(self):
-        yield 'HelloWorld'
-
-    def get_macro_description(self, name):
-        return inspect.getdoc(MacroListMacro)
-
-    def render_macro(self, req, name, content):
-        return 'Hello World, args = ' + content
-
-
-class WikiTestCase(unittest.TestCase):
-
-    def __init__(self, input, correct, file, line):
-        unittest.TestCase.__init__(self, 'test')
-        self.input = input
-        self.correct = correct
-        self.file = file
-        self.line = line
-    
-    def test(self):
-        """Testing WikiFormatter"""
-
-        # Environment stub
-        from trac.core import ComponentManager
-        from trac.config import Configuration
-        from trac.log import logger_factory
-        from trac.test import InMemoryDatabase
-        from trac.web.href import Href
-
-        db = InMemoryDatabase()
-
-        class DummyEnvironment(ComponentManager):
-            def __init__(self):
-                ComponentManager.__init__(self)
-                self.log = logger_factory('null')
-                self.config = Configuration(None)
-                self.href = Href('/')
-                self.abs_href = Href('http://www.example.com/')
-                self._wiki_pages = {}
-                self.path = ''
-            def component_activated(self, component):
-                component.env = self
-                component.config = self.config
-                component.log = self.log
-            def get_db_cnx(self):
-                return db
-
-        # Load all the components that provide IWikiSyntaxProvider
-        # implementations that are tested. Ideally those should be tested
-        # in separate unit tests.
-        import trac.versioncontrol.web_ui.browser
-        import trac.versioncontrol.web_ui.changeset
-        import trac.ticket.query
-        import trac.ticket.report
-        import trac.ticket.roadmap
-        import trac.Search
-
-        env = DummyEnvironment()
-
-        out = StringIO.StringIO()
-        formatter = self.formatter(env)
-        formatter.format(self.input, out)
-        v = out.getvalue().replace('\r','')
-        try:
-            self.assertEquals(self.correct, v)
-        except AssertionError, e:
-            raise AssertionError('%s\n\n%s:%s: for the input '
-                                 '(formatter flavor was "%s")' \
-                                 % (str(e), self.file, self.line,
-                                    formatter.flavor))
-        
-    def formatter(self, env):
-        return Formatter(env)
-
-
-class OneLinerTestCase(WikiTestCase):
-    def formatter(self, env):
-        return OneLinerFormatter(env)
-
-
-def suite():
-    suite = unittest.TestSuite()
-    file = os.path.join(os.path.split(__file__)[0], 'wiki-tests.txt')
-    data = open(file, 'r').read()
-    tests = data.split('=' * 30 + '\n')
-    line = 1
-    for test in tests:
-        input, page, oneliner = test.split('-' * 30 + '\n')
-        suite.addTest(WikiTestCase(input, page, file, line))
-        if oneliner:
-            suite.addTest(OneLinerTestCase(input, oneliner[:-1], file, line))
-        line += len(test.split('\n'))
-    return suite
-
-if __name__ == '__main__':
-    runner = unittest.TextTestRunner()
-    runner.run(suite())
+from __future__ import generators
+import os
+import inspect
+import StringIO
+import unittest
+
+from trac.core import *
+from trac.wiki.formatter import Formatter, OneLinerFormatter
+from trac.wiki.api import IWikiMacroProvider
+
+
+class DummyHelloWorldMacro(Component):
+    """
+    A dummy macro used by the unit test. We need to supply our own macro
+    because the real HelloWorld-macro can not be loaded using our
+    'fake' environment.
+    """
+    implements(IWikiMacroProvider)
+
+    def get_macros(self):
+        yield 'HelloWorld'
+
+    def get_macro_description(self, name):
+        return inspect.getdoc(MacroListMacro)
+
+    def render_macro(self, req, name, content):
+        return 'Hello World, args = ' + content
+
+
+class WikiTestCase(unittest.TestCase):
+
+    def __init__(self, input, correct, file, line):
+        unittest.TestCase.__init__(self, 'test')
+        self.input = input
+        self.correct = correct
+        self.file = file
+        self.line = line
+    
+    def test(self):
+        """Testing WikiFormatter"""
+
+        # Environment stub
+        from trac.core import ComponentManager
+        from trac.config import Configuration
+        from trac.log import logger_factory
+        from trac.test import InMemoryDatabase
+        from trac.web.href import Href
+
+        db = InMemoryDatabase()
+
+        class DummyEnvironment(ComponentManager):
+            def __init__(self):
+                ComponentManager.__init__(self)
+                self.log = logger_factory('null')
+                self.config = Configuration(None)
+                self.href = Href('/')
+                self.abs_href = Href('http://www.example.com/')
+                self.path = ''
+                # -- intertrac support
+                self.siblings = {}
+                self.config.set('intertrac', 'trac.title', "Trac's Trac")
+                self.config.set('intertrac', 'trac.url',
+                                "http://projects.edgewall.com/trac")
+                self.config.set('intertrac', 't', 'trac')
+            def component_activated(self, component):
+                component.env = self
+                component.config = self.config
+                component.log = self.log
+            def get_db_cnx(self):
+                return db
+
+        # Load all the components that provide IWikiSyntaxProvider
+        # implementations that are tested. Ideally those should be tested
+        # in separate unit tests.
+        import trac.versioncontrol.web_ui.browser
+        import trac.versioncontrol.web_ui.changeset
+        import trac.ticket.query
+        import trac.ticket.report
+        import trac.ticket.roadmap
+        import trac.Search
+
+        env = DummyEnvironment()
+
+        out = StringIO.StringIO()
+        formatter = self.formatter(env)
+        formatter.format(self.input, out)
+        v = out.getvalue().replace('\r','')
+        try:
+            self.assertEquals(self.correct, v)
+        except AssertionError, e:
+            raise AssertionError('%s\n\n%s:%s: for the input '
+                                 '(formatter flavor was "%s")' \
+                                 % (str(e), self.file, self.line,
+                                    formatter.flavor))
+        
+    def formatter(self, env):
+        return Formatter(env)
+
+
+class OneLinerTestCase(WikiTestCase):
+    def formatter(self, env):
+        return OneLinerFormatter(env)
+
+
+def suite():
+    suite = unittest.TestSuite()
+    file = os.path.join(os.path.split(__file__)[0], 'wiki-tests.txt')
+    data = open(file, 'r').read()
+    tests = data.split('=' * 30 + '\n')
+    line = 1
+    for test in tests:
+        input, page, oneliner = test.split('-' * 30 + '\n')
+        suite.addTest(WikiTestCase(input, page, file, line))
+        if oneliner:
+            suite.addTest(OneLinerTestCase(input, oneliner[:-1], file, line))
+        line += len(test.split('\n'))
+    return suite
+
+if __name__ == '__main__':
+    runner = unittest.TextTestRunner()
+    runner.run(suite())
Index: trac/wiki/formatter.py
===================================================================
--- trac/wiki/formatter.py	(revision 2797)
+++ trac/wiki/formatter.py	(working copy)
@@ -28,10 +28,11 @@
     from StringIO import StringIO
 
 from trac import util
+from trac.core import *
 from trac.mimeview import *
-from trac.wiki.api import WikiSystem
+from trac.wiki.api import WikiSystem, IWikiChangeListener, IWikiMacroProvider
 
-__all__ = ['wiki_to_html', 'wiki_to_oneliner', 'wiki_to_outline']
+__all__ = ['wiki_to_html', 'wiki_to_oneliner', 'wiki_to_outline', 'Formatter' ]
 
 
 def system_message(msg, text):
@@ -132,6 +133,7 @@
     INLINE_TOKEN = "`"
 
     LINK_SCHEME = r"[\w.+-]+" # as per RFC 2396
+    INTERTRAC_SCHEME = r"[a-zA-Z.+-]+?" # no digits (support for shorthand links)
 
     QUOTED_STRING = r"'[^']+'|\"[^\"]+\""
 
@@ -290,14 +292,66 @@
             return self._make_link(ns, target, match, label)
 
     def _make_link(self, ns, target, match, label):
+        # check first for an alias defined in trac.ini
+        ns = self.env.config.get('intertrac', ns.upper(), ns)
         if ns in self.link_resolvers:
             return self.link_resolvers[ns](self, ns, target,
                                            util.escape(label, False))
         elif target.startswith('//') or ns == "mailto":
             return self._make_ext_link(ns+':'+target, label)
         else:
-            return util.escape(match)
+            return self._make_intertrac_link(ns, target, label) or \
+                   self._make_interwiki_link(ns, target, label) or \
+                   match
 
+    def _make_intertrac_link(self, ns, target, label):
+        if self.env.siblings.has_key(ns):
+            sibling = self.env.siblings[ns]
+            # The following is currently needed because env.href is set
+            # in trac.web.main.dispatch_request: for an environment which
+            # has not yet been queried by a client, .href is not defined.
+            if not hasattr(sibling, 'href'):
+                from trac.web.href import Href
+                def xchg_base(base):
+                    return '/'.join(base.split('/')[:-1] + [ns])
+                sibling.href = Href(xchg_base(self.env.href.base))
+                sibling.abs_href = Href(xchg_base(self.env.abs_href.base))
+            # EOKludge
+            ref = wiki_to_oneliner(target, sibling)
+            return ref.replace('>%s' % target, '>%s' % label)
+        url = self.env.config.get('intertrac', ns.upper()+'.url')
+        if url:
+            name = self.env.config.get('intertrac', ns.upper()+'.title',
+                                       'Trac project %s' % ns)
+            sep = target.find(':')
+            if sep != -1:
+                url = '%s/%s/%s' % (url, target[:sep], target[sep+1:])
+            else: 
+                url = '%s/search?q=%s' % (url, urllib.quote_plus(target))
+            return self._make_ext_link(url, label, '%s in %s' % (target, name))
+        else:
+            return None
+
+    def shorthand_intertrac_helper(self, ns, target, label, fullmatch):
+        if fullmatch: # short form
+            it_group = fullmatch.group('it_%s' % ns)
+            if it_group:
+                alias = it_group.strip()
+                intertrac = self.env.config.get('intertrac', alias.upper(),
+                                                alias)
+                target = '%s:%s' % (ns, target[len(it_group):])
+                return self._make_intertrac_link(intertrac, target, label) or \
+                       label
+        return None
+
+    def _make_interwiki_link(self, ns, target, label):
+        interwiki = InterWikiMap(self.env)
+        if interwiki.has_key(ns):
+            url, title = interwiki.url(ns, target)
+            return self._make_ext_link(url, label, '%s in %s' % (target, title))
+        else:
+            return None
+
     def _make_ext_link(self, url, text, title=''):
         url = util.escape(url)
         text, title = util.escape(text), util.escape(title)
@@ -755,3 +809,98 @@
     OutlineFormatter(env, absurls, db).format(wikitext, out, max_depth,
                                               min_depth)
     return util.Markup(out.getvalue())
+
+
+# -- InterWiki support
+
+class InterWikiMap(Component):
+
+    implements(IWikiChangeListener, IWikiMacroProvider)
+
+    _page_name = 'InterMapTxt'
+    _interwiki_re = re.compile(r"(\w+)[ \t]+([^ \t]+)(?:[ \t]+#(.*))?",
+                               re.UNICODE)
+    _argspec_re = re.compile(r"\$\d")
+
+    def __init__(self):
+        self._interwiki_map = None
+        # This dictionary maps upper-cased namespaces
+        # to (namespace, prefix, title) values
+
+    def has_key(self, ns):
+        if not self._interwiki_map:
+            self._update()
+        return self._interwiki_map.has_key(ns.upper())
+
+    def url(self, ns, target):
+        ns, url, title = self._interwiki_map[ns.upper()]
+        args = target.split(':')
+        def setarg(match):
+            num = int(match.group()[1:])
+            return 0 < num <= len(args) and args[num-1] or ''
+        url_with_args = re.sub(InterWikiMap._argspec_re, setarg, url)
+        if url_with_args == url: 
+            return url + target, title
+        else:
+            return url_with_args, title
+
+    # IWikiChangeListener methods
+
+    def wiki_page_added(self, page):
+        if page.name == InterWikiMap._page_name:
+            self._update()
+
+    def wiki_page_changed(self, page, version, t, comment, author, ipnr):
+        if page.name == InterWikiMap._page_name:
+            self._update()
+
+    def wiki_page_deleted(self, page):
+        if page.name == InterWikiMap._page_name:
+            self._interwiki_map.clear()
+
+    def _update(self):
+        from trac.wiki.model import WikiPage
+        self._interwiki_map = {}
+        content = WikiPage(self.env, InterWikiMap._page_name).text
+        in_map = False
+        for line in content.split('\n'):
+            if in_map:
+                if line.startswith('----'):
+                    in_map = False
+                else:
+                    m = re.match(InterWikiMap._interwiki_re, line)
+                    if m:
+                        prefix, url, title = m.groups()
+                        url = url.strip()
+                        title = title and title.strip() or prefix
+                        self._interwiki_map[prefix.upper()] = (prefix, url,
+                                                               title)
+            elif line.startswith('----'):
+                in_map = True
+
+    # IWikiMacroProvider
+
+    def get_macros(self):
+        yield 'InterWiki'
+
+    def get_macro_description(self, name): 
+        return "Provide a description list for the known InterWiki prefixes."
+
+    def render_macro(self, req, name, content):
+        if not self._interwiki_map:
+            self._update()
+        keys = self._interwiki_map.keys()
+        keys.sort()
+        buf = StringIO()
+        buf.write('<table><tr><th>Prefix</th><td>Site</td></tr>\n')
+        for k in keys:
+            prefix, url, title = self._interwiki_map[k]
+            shortened_url = url and url[:-1]
+            description = title == prefix and shortened_url or title
+            buf.write('<tr>\n' +
+                      ('<td><a href="%sRecentChanges">%s</a></td>'
+                       '<td><a href="%s">%s</a></td>\n') \
+                      % (url, prefix, shortened_url, description) +
+                      '</tr>\n')
+        buf.write('</table>\n')
+        return buf.getvalue()
Index: trac/web/standalone.py
===================================================================
--- trac/web/standalone.py	(revision 2797)
+++ trac/web/standalone.py	(working copy)
@@ -24,6 +24,7 @@
 from trac.web.api import Request
 from trac.web.cgi_frontend import TracFieldStorage
 from trac.web.main import dispatch_request, get_environment, \
+                          setup_sibling_environments, \
                           send_pretty_error, send_project_index
 from trac.util import md5crypt
 
@@ -201,35 +202,15 @@
         else:
             self.http_host = '%s:%d' % (self.server_name, self.server_port)
 
-        self.env_parent_dir = env_parent_dir and {'TRAC_ENV_PARENT_DIR':
-                                                  env_parent_dir}
+        self.env_paths = env_paths
         self.auths = auths
+        self.options = os.environ.copy()
+        if env_parent_dir:
+            self.options['TRAC_ENV_PARENT_DIR'] = env_parent_dir
+        self.projects = setup_sibling_environments(self.options, self.env_paths)
 
-        self.projects = {}
-        for env_path in env_paths:
-            # Remove trailing slashes
-            while env_path and not os.path.split(env_path)[1]:
-                env_path = os.path.split(env_path)[0]
-            project = os.path.split(env_path)[1]
-            if self.projects.has_key(project):
-                print >>sys.stderr, 'Warning: Ignoring project "%s" since ' \
-                                    'it conflicts with project "%s"' \
-                                    % (env_path, self.projects[project])
-            else:
-                self.projects[project] = env_path
-
-    def get_env_opts(self, project=None):
-        if self.env_parent_dir:
-            opts = self.env_parent_dir.items()
-        else:
-            opts = [('TRAC_ENV', self.projects[project])]
-        return dict(opts + os.environ.items())
-
     def send_project_index(self, req):
-        if self.env_parent_dir:
-            return send_project_index(req, self.get_env_opts())
-        else:
-            return send_project_index(req, os.environ, self.projects.values())
+        return send_project_index(req, self.options, self.env_paths)
 
 
 class TracHTTPRequestHandler(BaseHTTPRequestHandler):
@@ -275,14 +256,12 @@
         path_info = urllib.unquote(path_info)
         req = TracHTTPRequest(self, project_name, query_string)
 
-        try:
-            opts = self.server.get_env_opts(project_name)
-        except KeyError:
-            # unrecognized project
-            self.server.send_project_index(req)
-            return
+        env = None
+        if project_name in self.server.projects:
+            options = self.server.options.copy()
+            options['TRAC_ENV'] = self.server.projects[project_name]
+            env = get_environment(req, options)
 
-        env = get_environment(req, opts)
         if not env:
             self.server.send_project_index(req)
             return
Index: trac/web/modpython_frontend.py
===================================================================
--- trac/web/modpython_frontend.py	(revision 2797)
+++ trac/web/modpython_frontend.py	(working copy)
@@ -31,6 +31,7 @@
 from trac.util import http_date
 from trac.web.api import Request, RequestDone
 from trac.web.main import dispatch_request, get_environment, \
+                          setup_sibling_environments, \
                           send_pretty_error, send_project_index
 
 
@@ -196,6 +197,7 @@
             ('TracEnvParentDir', 'TRAC_ENV_PARENT_DIR'),
             ('TracEnvIndexTemplate', 'TRAC_ENV_INDEX_TEMPLATE'),
             ('TracTemplateVars', 'TRAC_TEMPLATE_VARS'))
+    setup_sibling_environments(project_opts)
     env = get_environment(mpr, project_opts)
     if not env:
         send_project_index(mpr, project_opts)
Index: trac/web/main.py
===================================================================
--- trac/web/main.py	(revision 2797)
+++ trac/web/main.py	(working copy)
@@ -17,6 +17,8 @@
 #         Matthew Good <trac@matt-good.net>
 
 import os
+import sys
+import dircache
 
 from trac.core import *
 from trac.env import open_environment
@@ -279,24 +281,17 @@
    <a href="<?cs var:project.href ?>" title="<?cs var:project.description ?>">
     <?cs var:project.name ?></a><?cs
   else ?>
-   <small><?cs var:project.name ?>: <em>Error</em> <br />
-   (<?cs var:project.description ?>)</small><?cs
+   <?cs var:project.name ?>
+   <small><em><?cs var:project.description ?></em></small><?cs
   /if ?>
   </li><?cs
  /each ?></ul></body>
 </html>''')
 
-    if not env_paths and 'TRAC_ENV_PARENT_DIR' in options:
-        dir = options['TRAC_ENV_PARENT_DIR']
-        env_paths = [os.path.join(dir, f) for f in os.listdir(dir)]
-
     href = Href(req.idx_location)
     try:
         projects = []
-        for env_path in env_paths:
-            if not os.path.isdir(env_path):
-                continue
-            env_dir, project = os.path.split(env_path)
+        for project, env_path in get_projects(options, env_paths).items():
             try:
                 env = _open_environment(env_path)
                 proj = {
@@ -322,7 +317,7 @@
     elif 'TRAC_ENV_PARENT_DIR' in options:
         env_parent_dir = options['TRAC_ENV_PARENT_DIR']
         env_name = req.cgi_location.split('/')[-1]
-        env_path = os.path.join(env_parent_dir, env_name)
+        env_path = os.path.join(os.path.normpath(env_parent_dir), env_name)
         if not len(env_name) or not os.path.exists(env_path):
             return None
     else:
@@ -333,3 +328,53 @@
               'the Trac environment(s).'
 
     return _open_environment(env_path, threaded)
+
+
+def get_projects(options, env_paths, warn=False):
+    """Retrieve canonical project to environment path mapping.
+
+    The environments may not be all valid environments, though,
+    but they are serious candidates...
+    """
+    env_paths = env_paths or []
+    if 'TRAC_ENV_PARENT_DIR' in options:
+        env_parent_dir = os.path.normpath(options['TRAC_ENV_PARENT_DIR'])
+        if env_parent_dir:
+            paths = dircache.listdir(env_parent_dir)[:]
+            dircache.annotate(env_parent_dir, paths)
+            env_paths += [os.path.join(env_parent_dir, project) \
+                          for project in paths if project[-1] == '/']
+    projects = {}
+    for env_path in env_paths:
+        env_path = os.path.normpath(env_path)
+        if not os.path.isdir(env_path):
+            continue
+        project = os.path.split(env_path)[1]
+        if project in projects:
+            if warn:
+                print >>sys.stderr,('Warning: Ignoring project "%s" since ' \
+                                    'it conflicts with project "%s"' \
+                                    % (env_path, projects[project]))
+        else:
+            projects[project] = env_path
+    return projects
+
+def setup_sibling_environments(options, env_paths=None):
+    """Make each of the given environment know all the others.
+
+    The environments can be specified by a list of `env_paths`
+    and also from the `TRAC_ENV_PARENT_DIR` options.
+    Return a mapping of project names to environment paths.
+    """
+    siblings = {}
+    projects = get_projects(options, env_paths, warn=True)
+    options = options.copy()
+    for project, env_path in projects.items():
+        options['TRAC_ENV'] = env_path
+        try:
+            siblings[project] = get_environment(None, options)
+        except (TracError, IOError):
+            pass
+    for env in siblings.values():
+        env.siblings = siblings
+    return projects
