changeset 39:93b4dcbafd7b trunk

Copy Trac to main branch.
author cmlenz
date Mon, 03 Jul 2006 18:53:27 +0000
parents ee669cb9cccc
children f8a5a6ee2097
files examples/trac/AUTHORS examples/trac/COPYING examples/trac/ChangeLog examples/trac/INSTALL examples/trac/MANIFEST.in examples/trac/README examples/trac/README.tracd examples/trac/RELEASE examples/trac/THANKS examples/trac/UPGRADE examples/trac/cgi-bin/trac.cgi examples/trac/cgi-bin/trac.fcgi examples/trac/contrib/README examples/trac/contrib/bugzilla2trac.py examples/trac/contrib/emailfilter.py examples/trac/contrib/migrateticketmodel.py examples/trac/contrib/sourceforge2trac.py examples/trac/contrib/trac-post-commit-hook examples/trac/contrib/trac-pre-commit-hook examples/trac/doc/README examples/trac/doc/trac_icon_16x16.png examples/trac/doc/trac_icon_32x32.png examples/trac/doc/trac_logo.png examples/trac/doc/trac_logo.svg examples/trac/htdocs/README examples/trac/htdocs/asc.png examples/trac/htdocs/attachment.png examples/trac/htdocs/changeset.png examples/trac/htdocs/closedticket.png examples/trac/htdocs/css/about.css examples/trac/htdocs/css/browser.css examples/trac/htdocs/css/changeset.css examples/trac/htdocs/css/code.css examples/trac/htdocs/css/diff.css examples/trac/htdocs/css/report.css examples/trac/htdocs/css/roadmap.css examples/trac/htdocs/css/search.css examples/trac/htdocs/css/ticket.css examples/trac/htdocs/css/timeline.css examples/trac/htdocs/css/trac.css examples/trac/htdocs/css/wiki.css examples/trac/htdocs/desc.png examples/trac/htdocs/dots.gif examples/trac/htdocs/draft.png examples/trac/htdocs/edgewall.png examples/trac/htdocs/edit_toolbar.png examples/trac/htdocs/editedticket.png examples/trac/htdocs/extlink.gif examples/trac/htdocs/file.png examples/trac/htdocs/filedeny.png examples/trac/htdocs/folder.png examples/trac/htdocs/folderdeny.png examples/trac/htdocs/ics.png examples/trac/htdocs/imggrid.png examples/trac/htdocs/js/query.js examples/trac/htdocs/js/trac.js examples/trac/htdocs/js/wikitoolbar.js examples/trac/htdocs/milestone.png examples/trac/htdocs/newticket.png examples/trac/htdocs/parent.png examples/trac/htdocs/topbar_gradient.png examples/trac/htdocs/topbar_gradient2.png examples/trac/htdocs/trac.ico examples/trac/htdocs/trac_banner.png examples/trac/htdocs/trac_logo_mini.png examples/trac/htdocs/wiki.png examples/trac/htdocs/xml.png examples/trac/scripts/rpm-install.sh examples/trac/scripts/trac-admin examples/trac/scripts/trac-admin.1 examples/trac/scripts/trac-postinstall.py examples/trac/scripts/tracd examples/trac/scripts/tracd.1 examples/trac/scripts/tracdb2env examples/trac/scripts/tracdb2env.1 examples/trac/setup.py examples/trac/setup_wininst.bmp examples/trac/templates/README examples/trac/templates/about.cs examples/trac/templates/anydiff.cs examples/trac/templates/attachment.cs examples/trac/templates/browser.cs examples/trac/templates/changeset.cs examples/trac/templates/error.cs examples/trac/templates/footer.cs examples/trac/templates/header.cs examples/trac/templates/index.cs examples/trac/templates/log.cs examples/trac/templates/log_changelog.cs examples/trac/templates/log_rss.cs examples/trac/templates/macros.cs examples/trac/templates/milestone.cs examples/trac/templates/newticket.cs examples/trac/templates/query.cs examples/trac/templates/query_rss.cs examples/trac/templates/report.cs examples/trac/templates/report_rss.cs examples/trac/templates/roadmap.cs examples/trac/templates/search.cs examples/trac/templates/settings.cs examples/trac/templates/ticket.cs examples/trac/templates/ticket_notify_email.cs examples/trac/templates/ticket_rss.cs examples/trac/templates/timeline.cs examples/trac/templates/timeline_rss.cs examples/trac/templates/wiki.cs examples/trac/trac/About.py examples/trac/trac/Search.py examples/trac/trac/Settings.py examples/trac/trac/Timeline.py examples/trac/trac/__init__.py examples/trac/trac/attachment.py examples/trac/trac/config.py examples/trac/trac/core.py examples/trac/trac/db/__init__.py examples/trac/trac/db/api.py examples/trac/trac/db/mysql_backend.py examples/trac/trac/db/pool.py examples/trac/trac/db/postgres_backend.py examples/trac/trac/db/schema.py examples/trac/trac/db/sqlite_backend.py examples/trac/trac/db/tests/__init__.py examples/trac/trac/db/tests/api.py examples/trac/trac/db/util.py examples/trac/trac/db_default.py examples/trac/trac/env.py examples/trac/trac/loader.py examples/trac/trac/log.py examples/trac/trac/mimeview/__init__.py examples/trac/trac/mimeview/api.py examples/trac/trac/mimeview/enscript.py examples/trac/trac/mimeview/patch.py examples/trac/trac/mimeview/php.py examples/trac/trac/mimeview/rst.py examples/trac/trac/mimeview/silvercity.py examples/trac/trac/mimeview/tests/__init__.py examples/trac/trac/mimeview/tests/api.py examples/trac/trac/mimeview/txtl.py examples/trac/trac/notification.py examples/trac/trac/perm.py examples/trac/trac/scripts/__init__.py examples/trac/trac/scripts/admin.py examples/trac/trac/scripts/tests/__init__.py examples/trac/trac/scripts/tests/admin-tests.txt examples/trac/trac/scripts/tests/admin.py examples/trac/trac/test.py examples/trac/trac/tests/__init__.py examples/trac/trac/tests/allwiki.py examples/trac/trac/tests/attachment.py examples/trac/trac/tests/config.py examples/trac/trac/tests/core.py examples/trac/trac/tests/env.py examples/trac/trac/tests/notification.py examples/trac/trac/tests/perm.py examples/trac/trac/tests/wikisyntax.py examples/trac/trac/ticket/__init__.py examples/trac/trac/ticket/api.py examples/trac/trac/ticket/model.py examples/trac/trac/ticket/notification.py examples/trac/trac/ticket/query.py examples/trac/trac/ticket/report.py examples/trac/trac/ticket/roadmap.py examples/trac/trac/ticket/tests/__init__.py examples/trac/trac/ticket/tests/api.py examples/trac/trac/ticket/tests/conversion.py examples/trac/trac/ticket/tests/model.py examples/trac/trac/ticket/tests/notification.py examples/trac/trac/ticket/tests/query.py examples/trac/trac/ticket/tests/wikisyntax.py examples/trac/trac/ticket/web_ui.py examples/trac/trac/upgrades/__init__.py examples/trac/trac/upgrades/db10.py examples/trac/trac/upgrades/db11.py examples/trac/trac/upgrades/db12.py examples/trac/trac/upgrades/db13.py examples/trac/trac/upgrades/db14.py examples/trac/trac/upgrades/db15.py examples/trac/trac/upgrades/db16.py examples/trac/trac/upgrades/db17.py examples/trac/trac/upgrades/db18.py examples/trac/trac/upgrades/db19.py examples/trac/trac/upgrades/db3.py examples/trac/trac/upgrades/db4.py examples/trac/trac/upgrades/db5.py examples/trac/trac/upgrades/db6.py examples/trac/trac/upgrades/db7.py examples/trac/trac/upgrades/db8.py examples/trac/trac/upgrades/db9.py examples/trac/trac/util/__init__.py examples/trac/trac/util/autoreload.py examples/trac/trac/util/daemon.py examples/trac/trac/util/datefmt.py examples/trac/trac/util/markup.py examples/trac/trac/util/tests/__init__.py examples/trac/trac/util/tests/markup.py examples/trac/trac/util/tests/text.py examples/trac/trac/util/text.py examples/trac/trac/versioncontrol/__init__.py examples/trac/trac/versioncontrol/api.py examples/trac/trac/versioncontrol/cache.py examples/trac/trac/versioncontrol/diff.py examples/trac/trac/versioncontrol/svn_authz.py examples/trac/trac/versioncontrol/svn_fs.py examples/trac/trac/versioncontrol/tests/__init__.py examples/trac/trac/versioncontrol/tests/cache.py examples/trac/trac/versioncontrol/tests/diff.py examples/trac/trac/versioncontrol/tests/svn_authz.py examples/trac/trac/versioncontrol/tests/svn_fs.py examples/trac/trac/versioncontrol/tests/svnrepos.dump examples/trac/trac/versioncontrol/web_ui/__init__.py examples/trac/trac/versioncontrol/web_ui/browser.py examples/trac/trac/versioncontrol/web_ui/changeset.py examples/trac/trac/versioncontrol/web_ui/log.py examples/trac/trac/versioncontrol/web_ui/tests/__init__.py examples/trac/trac/versioncontrol/web_ui/tests/wikisyntax.py examples/trac/trac/versioncontrol/web_ui/util.py examples/trac/trac/web/__init__.py examples/trac/trac/web/_fcgi.py examples/trac/trac/web/api.py examples/trac/trac/web/auth.py examples/trac/trac/web/cgi_frontend.py examples/trac/trac/web/chrome.py examples/trac/trac/web/clearsilver.py examples/trac/trac/web/fcgi_frontend.py examples/trac/trac/web/href.py examples/trac/trac/web/main.py examples/trac/trac/web/modpython_frontend.py examples/trac/trac/web/session.py examples/trac/trac/web/standalone.py examples/trac/trac/web/tests/__init__.py examples/trac/trac/web/tests/api.py examples/trac/trac/web/tests/auth.py examples/trac/trac/web/tests/cgi_frontend.py examples/trac/trac/web/tests/chrome.py examples/trac/trac/web/tests/clearsilver.py examples/trac/trac/web/tests/href.py examples/trac/trac/web/tests/session.py examples/trac/trac/web/tests/wikisyntax.py examples/trac/trac/web/wsgi.py examples/trac/trac/wiki/__init__.py examples/trac/trac/wiki/api.py examples/trac/trac/wiki/formatter.py examples/trac/trac/wiki/interwiki.py examples/trac/trac/wiki/macros.py examples/trac/trac/wiki/model.py examples/trac/trac/wiki/tests/__init__.py examples/trac/trac/wiki/tests/formatter.py examples/trac/trac/wiki/tests/macros.py examples/trac/trac/wiki/tests/model.py examples/trac/trac/wiki/tests/wiki-tests.txt examples/trac/trac/wiki/tests/wikisyntax.py examples/trac/trac/wiki/web_ui.py examples/trac/wiki-default/CamelCase examples/trac/wiki-default/InterMapTxt examples/trac/wiki-default/InterTrac examples/trac/wiki-default/InterWiki examples/trac/wiki-default/RecentChanges examples/trac/wiki-default/SandBox examples/trac/wiki-default/TitleIndex examples/trac/wiki-default/TracAccessibility examples/trac/wiki-default/TracAdmin examples/trac/wiki-default/TracBackup examples/trac/wiki-default/TracBrowser examples/trac/wiki-default/TracCgi examples/trac/wiki-default/TracChangeset examples/trac/wiki-default/TracEnvironment examples/trac/wiki-default/TracFastCgi examples/trac/wiki-default/TracGuide examples/trac/wiki-default/TracImport examples/trac/wiki-default/TracIni examples/trac/wiki-default/TracInstall examples/trac/wiki-default/TracInterfaceCustomization examples/trac/wiki-default/TracLinks examples/trac/wiki-default/TracLogging examples/trac/wiki-default/TracModPython examples/trac/wiki-default/TracNotification examples/trac/wiki-default/TracPermissions examples/trac/wiki-default/TracPlugins examples/trac/wiki-default/TracQuery examples/trac/wiki-default/TracReports examples/trac/wiki-default/TracRevisionLog examples/trac/wiki-default/TracRoadmap examples/trac/wiki-default/TracRss examples/trac/wiki-default/TracSearch examples/trac/wiki-default/TracStandalone examples/trac/wiki-default/TracSupport examples/trac/wiki-default/TracSyntaxColoring examples/trac/wiki-default/TracTickets examples/trac/wiki-default/TracTicketsCustomFields examples/trac/wiki-default/TracTimeline examples/trac/wiki-default/TracUnicode examples/trac/wiki-default/TracUpgrade examples/trac/wiki-default/TracWiki examples/trac/wiki-default/WikiDeletePage examples/trac/wiki-default/WikiFormatting examples/trac/wiki-default/WikiHtml examples/trac/wiki-default/WikiMacros examples/trac/wiki-default/WikiNewPage examples/trac/wiki-default/WikiPageNames examples/trac/wiki-default/WikiProcessors examples/trac/wiki-default/WikiRestructuredText examples/trac/wiki-default/WikiRestructuredTextLinks examples/trac/wiki-default/WikiStart examples/trac/wiki-default/checkwiki.py examples/trac/wiki-macros/HelloWorld.py examples/trac/wiki-macros/Timestamp.py examples/trac/wiki-macros/TracGuideToc.py
diffstat 307 files changed, 44454 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/examples/trac/AUTHORS
@@ -0,0 +1,12 @@
+ * Jonas Borgström <jonas@edgewall.com>
+ * Daniel Lundin <daniel@edgewall.com>
+ * Rocky Burt <rocky@carterscove.com>
+ * Christopher Lenz <cmlenz@gmx.de>
+ * Francois Harvey <fharvey@securiweb.net>
+ * Mark Rowe <trac@bdash.net.nz> 
+ * Matthew Good <trac@matt-good.net>
+ * Christian Boos <cboos@neuf.fr>
+ * Emmanual Blot <emmanuel.blot@free.fr>
+ * Alec Thomas <alec@swapoff.org>
+
+See also THANKS for people who have contributed to the project.
new file mode 100644
--- /dev/null
+++ b/examples/trac/COPYING
@@ -0,0 +1,28 @@
+Copyright (C) 2003-2006 Edgewall Software
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+ 1. Redistributions of source code must retain the above copyright
+    notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+    notice, this list of conditions and the following disclaimer in
+    the documentation and/or other materials provided with the
+    distribution.
+ 3. The name of the author may not be used to endorse or promote
+    products derived from this software without specific prior
+    written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
new file mode 100644
--- /dev/null
+++ b/examples/trac/ChangeLog
@@ -0,0 +1,297 @@
+Trac 0.9.5  (Apr 18, 2006)
+http://svn.edgewall.com/repos/trac/tags/trac-0.9.5
+
+ * Fixed wiki macro XSS vulnerability found by Mr. Kazuhiro Nishiyama:
+   http://jvn.jp/jp/JVN%2384091359/index.html
+ * Smaller memory usage when accessing subversion history.
+ * Fixed issue with incorrectly generated urls when installed behind a web 
+   proxy (#2531).
+ * Fixed bugs: #2531, #2777, #3020.
+	
+Trac 0.9.4  (Feb 15, 2006)
+http://svn.edgewall.com/repos/trac/tags/trac-0.9.4
+
+ * Deletion of reports has been fixed.
+ * Various encoding issues with the timeline RSS feed have been fixed.
+ * Fixed a memory leak when syncing with the repository.
+ * Milestones in the roadmap are now ordered more intelligently.
+ * Fixed bugs: #1064, #1150, #2006, #2253, #2324, #2330, #2408, #2430,
+   #2431, #2459, #2544, #2459, #2481, #2485, #2536, #2544, #2553,
+   #2580, #2583, #2606, #2613, #2621, #2664, #2666, #2680, #2706,
+   #2707, #2735
+
+Trac 0.9.3  (Jan 8, 2006)
+http://svn.edgewall.com/repos/trac/tags/trac-0.9.3
+
+ * Fixed XSS vulnerabilities.
+ * Timeline RSS feed validity issue resolved.
+ * "trac-admin initenv" now handles empty repositories.
+ * Textile unicode support.
+ * Fixed bugs: #1158, #2290, #2337, #2416, #2440, #2468, #2473, #2484,
+   #2490, #2493, #2512, #2517, #2519, #2527, #2548, #2558, #2558
+
+Trac 0.9.2  (Dec 5, 2005)
+http://svn.edgewall.com/repos/trac/tags/trac-0.9.2
+
+ * Fixed SQL injection vulnerability in ticket search module.
+ * Fixed broken ticket email notifications.
+
+Trac 0.9.1  (Dec 1, 2005)
+http://svn.edgewall.com/repos/trac/tags/trac-0.9.1
+
+ * Fixed SQL injection vulnerability in ticket query module.
+ * Fixed bugs: #1633, #2167, #2283, #2284, #2285, #2291, #2292, #2300,
+   #2318, #2329, #2366, #2369, #2373, #2383, #2416, #2457
+
+
+Trac 0.9  (Oct 31, 2005)
+http://svn.edgewall.com/repos/trac/tags/trac-0.9
+
+ * Support for a global trac.ini configuration file.
+ * Changed logic for enabling plugins/components.
+ * Improved support for handling repository subsets.
+ * Fixes problems with Trac links when using multiple environments in the
+   same Python interpreter.
+ * Improvements to email notification layout and encoding.
+ * Fixes for database locking with SQLite, in particular in a multi-threaded
+   environment.
+ * PostgreSQL compatibility fixes.
+ * Fixed bugs: #804, #861, #927, #1044, #1051, #1123, #1153, #1169,
+   #1239, #1344, #1463, #1562, #1881, #1886, #1895, #1909, #1921, #1930,
+   #1983, #1988, #2019, #2051, #2061, #2229, #2106, #2107, #2116, #2120,
+   #2124, #2129, #2135, #2136, #2138, #2140, #2144, #2164, #2166, #2170,
+   #2172, #2191, #2192, #2196, #2201, #2202, #2203, #2208, #2215, #2218,
+   #2223, #2230, #2232, #2239, #2240, #2241, #2243, #2251, 
+
+
+Trac 0.9-beta2  (Sept 25, 2005)
+http://svn.edgewall.com/repos/trac/tags/trac-0.9b2
+
+ * Support for setuptools 0.6.
+ * Allow insertion of a custom HTML snippet above the new ticket form
+   to explain site-specific policies and/or guidelines.
+ * Much improved Bugzilla import script.
+ * Fixed a bug where deleting a wiki page version would sometimes delete
+   the entire page.
+ * Fixes for the rendering of diffs and patches.
+ * Fixes for the Subversion authz support.
+ * Fixed bugs: #2008, #2032, #2034, #1801, #1893, #1040, #2040,
+   #1036, #1944, #1081, #1863, #2052, #2066, #2016, #2090, #1985,
+   #2012, #2089, #2079, #1999, #2029, #2079, #1960, #2080, #2021,
+   #2042, #2088, #1345, #2011, #2100, #2103, #2113, #2116, #2109
+
+
+Trac 0.9-beta1  (Sept 5, 2005)
+http://svn.edgewall.com/repos/trac/tags/trac-0.9b1
+
+ Trac 0.9 contains a great number of new features, improvements and
+ bug fixes. The following list contains only a few highlights:
+
+ * License changed from GPL to modified BSD (See the file COPYING).
+ * Improved modularity and extendibility (plugin support).
+ * Support for both pysqlite 1.x and pysqlite 2.x.
+ * Postgresql database support (with psycopg or pyPgSQL).
+ * Repository subsets. Multiple Trac environments can share a single 
+   repository.
+ * Version control abstraction layer making it possible to support 
+   other version control systems besides subversion in the future.
+ * FastCGI frontend support.
+ * Python version >= 2.3 is now required.
+
+ The complete list of closed tickets can be found here:
+   http://projects.edgewall.com/trac/query?status=closed&milestone=0.9
+
+
+Trac 0.8.3  (Jun 15, 2005)
+http://svn.edgewall.com/repos/trac/tags/trac-0.8.3
+
+ * Fix compatibility of 'trac-admin resync' with Subversion >= 1.2.
+ * Settings page now works correctly when Trac is deployed at the
+   root of a host.
+ * Windows packaging issues resolved.
+ * Fixed bugs: #1282, #1500, #1648
+
+
+Trac 0.8.2  (Jun 1, 2005)
+http://svn.edgewall.com/repos/trac/tags/trac-0.8.2
+
+ * Compatibility with Subversion >= 1.2 fixed.
+ * Compatibility with Docutils >= 0.3.7 fixed.
+ * Fixed bugs: #1020, #1302, #1500, #1182, #1339, #1518
+   #1525, #1618
+
+
+Trac 0.8.1  (Feb 28, 2005)
+http://svn.edgewall.com/repos/trac/tags/trac-0.8.1
+
+ * Improved Python 2.1 compatibility.
+ * Layout of navigation bar in Opera fixed.
+ * Execution of Javascript through event handler attributes
+   in HTML code is now forbidden.
+ * Fixed bugs: #157, #371, #556, #683, #970, #971, #972,
+   #974, #979, #983, #1001, #1003, #1007, #1008, #1011,
+   #1020, #1026, #1030, #1045, #1054, #1070, #1072, #1074,
+   #1076, #1087, #1090, #1103, #1108, #1111, #1136, #1159,
+   #1164, #1190, #1195, #1220
+
+
+Trac 0.8 'Qualia'  (Nov 15, 2004)
+http://svn.edgewall.com/repos/trac/tags/trac-0.8
+
+ * Roadmap module.
+ * Support for custom ticket properties.
+ * Wiki administration features.
+ * Advanced ticket queries.
+ * Improved diff display.
+ * User preferences.
+ * Wiki editing (near-wysiwyg) aids a la wikipedia.
+ * Improved email notification.
+ * Fixed bugs: #13, #63, #99, #100, #158, #164, #203, #210, 
+   #225, #264, #304, #306, #326, #346, #347, #351, 
+   #352, #364, #373, #375, #405, #411, #416, #431, 
+   #433, #434, #436, #438, #443, #445, #446, #447, 
+   #450, #452, #453, #455, #458, #460, #465, #468, 
+   #471, #472, #473, #474, #477, #478, #479, #480, 
+   #482, #483, #486, #487, #489, #491, #492, #494, 
+   #496, #501, #503, #506, #510, #512, #513, #514, 
+   #516, #522, #524, #526, #527, #528, #530, #532, 
+   #536, #537, #538, #539, #542, #543, #545, #546, 
+   #550, #551, #552, #553, #555, #556, #557, #558, 
+   #559, #560, #565, #567, #568, #570, #572, #574, 
+   #577, #578, #580, #581, #583, #587, #589, #591, 
+   #593, #594, #597, #598, #599, #600, #601, #602, 
+   #606, #609, #610, #612, #613, #616, #618, #619, 
+   #620, #622, #623, #626, #627, #628, #630, #631, 
+   #634, #644, #647, #648, #651, #652, #657, #658, 
+   #660, #664, #668, #669, #670, #671, #674, #675, 
+   #676, #677, #678, #680, #690, #692, #696, #698, 
+   #699, #703, #705, #706, #708, #709, #713, #714, 
+   #715, #716, #718, #720, #721, #722, #726, #727, 
+   #730, #732, #734, #735, #736, #737, #738, #741, 
+   #742, #743, #744, #745, #748, #749, #750, #751, 
+   #752, #759, #762, #764, #768, #769, #770, #771, 
+   #774, #775, #776, #778, #779, #780, #785, #789, 
+   #793, #798, #800, #806, #807, #815, #816, #817, 
+   #818, #829, #830, #831, #833, #836, #844, #846, 
+   #848, #850, #851, #852, #872, #873, #877, #878, 
+   #885, #888, #889, #892, #901, #903, #907, #912, 
+   #916, #923, #929, #931, #932, #935
+
+
+Trac 0.7.1 'Argento'  (Jun, 2004)
+http://svn.edgewall.com/repos/trac/tags/trac-0.7.1
+
+ * Bugfixes for 0.7
+ * Fixes security hole in auth.py
+ * Experimental support for mod_python
+ * Improved MIME-types
+ * Fixed bugs: #93, #202, #307, #312, #342, #345, #350, #353, #355, #391,
+   #393, #401, #404, #406, #415, #417, #419, #420, #421, #422, #424,
+   #425, #428, #429, #432, #435, #437, #441, #442, #448, #451, #452,
+   #456, #457, #461, #463, #466, #467, #470, #497, #498, #502, #504
+
+
+Trac 0.7 'Fulci'  (May 18, 2004)
+http://svn.edgewall.com/repos/trac/tags/trac-0.7
+
+ * Revised database format (requires manual upgrade).
+ * Trac standalone daemon, tracd (Experimental).
+ * Greatly improved browser.
+ * Many usability improvements.
+ * Clean-up of CSS and templates.
+ * UTF-8 character encoding support.
+ * Wiki page attachments.
+ * Syntax coloring supporting >35 languages, using SilverCity or GNU Enscript.
+ * Better support for ReStructuredText.
+ * Logging support, including syslog and windows eventlog.
+ * Ticket attachments.
+ * Import tickets from Bugzilla (contributed by Mark Rowe).
+ * Import tickets from SourceForge (contributed by Dmitry Yusupov).
+ * New ticket field: keywords
+ * Ticket email notification.
+ * Localized date and time display.
+ * Viewable SQL for reports.
+ * Improved search facilities.
+ * Windows installer package.
+ * More documentation.
+ * Fixed bugs: #14, #19, #27, #62, #87, #96, #106, #111, #115, #127, #146,
+   #161, #166, #171, #180, #182, #183, #188, #190, #191, #192, #193,
+   #195, #196, #197, #201, #205, #207, #211, #212, #213, #220, #224,
+   #227, #228, #231, #233, #235, #236, #240, #241, #243, #244, #246,
+   #247, #248, #249, #251, #252, #253, #254, #255, #258, #259, #261,
+   #262, #263, #265, #270, #271, #273, #275, #277, #278, #281, #284,
+   #285,  #88, #289, #292, #293, #294, #296, #300, #302, #310, #313,
+   #314, #315, #316, #320, #322, #328, #332, #333, #337, #338, #339,
+   #340, #341, #344, #348, #349, #358, #361, #362, #363, #368, #370,
+   #371, #372, #376, #377, #378, #381, #384, #385, #386, #387, #388,
+   #392, #394, #396, #397, #398, #399, #402, #403, #410
+
+
+Trac 0.6.1 '245 Trioxin (April 12, 2004)
+http://svn.edgewall.com/repos/trac/tags/trac-0.6.1
+
+ * RSS now escapes entities in summary.
+ * Search results won't highlight dates anymore.
+ * RPM for SuSE Linux.
+ * Preliminary Windows Installer.
+ * More documentation.
+ * Fixed bugs: #163, #165, #189, #198, #200, #206, #209, #214, #223
+
+
+Trac 0.6 'Solanum' (March 23, 2004)
+http://svn.edgewall.com/repos/trac/tags/trac-0.6
+
+ * View diffs between wiki page edits.
+ * Improved Search module.
+ * Support for tables in the wiki. (Thanks to Stephen Hansen)
+ * Colored reports. Use colors to show priority, etc. 
+ * Support for custom wiki processor macros.
+ * ReStructuredText markup support (through a processor macro)
+ * HTML markup support (through a processor macro)
+ * Report groups. Group results by a column.
+ * Multi-line report rows.
+ * Download report in CSV (Comma Separated Value) and tab-separated format
+ * RSS 2.0 content syndication support in Timeline, Reports and Log/Browser
+ * Better, locale-based date and time formatting. 
+ * Wiki RecentChanges support.
+ * Overall usability, consistency and cosmetic improvements.
+ * More documentation.
+ * Fixed bugs: #16, #68, #81, #88, #98, #101, #102, #103, #104, #105,
+   #110, #112, #113, #114, #117, #119, #120, #131, #132, #134,
+   #135, #136, #138, #142, #145, #147, #151, #155, #170, #173,
+   #174, #175, #177, #179
+
+
+Trac 0.5.2 'Nameless' (March 2, 2004)
+http://svn.edgewall.com/repos/trac/tags/trac-0.5.2
+
+ * Performance improvements.
+ * Better unicode support in commit-messages.
+ * TRAC_ADMIN is now a real "meta-permission" containing all other permissions.
+ * Wiki-links of the svn:/path format can now also link to directories.
+ * Handle subversion changesets without any "author" specified.
+ * "view" checkboxes in the timeline view now reflect the current state.
+ * The subversion repository is now indexed by "svnadmin initdb" instead of
+   trac.cgi at first execution.
+ * trac-admin now has a 'wiki dump' and 'wiki load' commands to
+    export/import all pages to/from a directory.
+ * Most of the inline css is removed.
+ * IE6 navbar problem fixed.
+ * Fixed bugs: #69, #73, #77, #78, #79, #80, #84, #85, #86, #89, #90, 
+   #91, #93, #97
+
+
+Trac 0.5.1 'Unnamed' (February 25, 2004)
+http://svn.edgewall.com/repos/trac/tags/trac-0.5.1
+
+ * Navbar now works properly on error pages.
+ * Cleaned up source code. Removed ugly tabs.
+ * Added missing COPYING, AUTHORS etc. Cleaned up package.
+ * trac-admin now works with python 2.1.
+ * Fixed bugs: #74, #75, #76, #77
+
+
+Trac 0.5 'Incognito' (February 23, 2004)
+http://svn.edgewall.com/repos/trac/tags/trac-0.5
+
+ * First release.
new file mode 100644
--- /dev/null
+++ b/examples/trac/INSTALL
@@ -0,0 +1,114 @@
+Trac Installation Guide
+=======================
+Trac is a lightweight project management tool that is implemented as a
+web-based application. Trac is written in the Python programming language and
+can use SQLite or PostgreSQL as  database. For HTML rendering, Trac uses the
+Clearsilver templating system.
+
+
+Requirements
+------------
+To install Trac, the following software packages must be installed:
+
+ * Python, version >= 2.3.
+   + Please keep in mind, that for RPM-based systems you will also need
+   python-devel and python-xml packages.
+ * Subversion, version >= 1.0. (>= 1.1.x recommended)
+ * Subversion SWIG Python bindings (not PySVN).
+ * PySQLite, version >= 0.5 (1.1.6 recommended)
+ * Clearsilver, version >= 0.9.3 (0.9.14 recommended)
+ * A web server capable of executing CGI/FastCGI scripts, or Apache HTTPD with
+   mod_python. (Trac also comes with a standalone server, but its use is not
+   recommended for use in a production environment.)
+
+
+Installing Trac
+---------------
+The command:
+
+  $ python ./setup.py install
+
+will byte-compile the python source code and install it in the
+site-packages directory of your python installation. The directories cgi-bin,
+templates, htdocs and wiki-default are all copied to ``$prefix/share/trac/``.
+
+The script will also install the trac-admin command-line tool, used to create
+and maintain project environments. Trac-admin is the command center of Trac.
+
+Note: you'll need root permissions or equivalent for this step.
+
+To install Trac in a different location, or use other advanced installation
+options, run:
+
+  $ python ./setup.py --help
+
+
+Installing Trac on Windows
+--------------------------
+If you downloaded the Trac installer (the .exe file), installing is simply a
+matter of running the installer.  After running the installer, configuration
+and installation is the same as for other platforms.
+
+
+Creating a Project Environment
+------------------------------
+A Trac environment is the backend storage format where Trac stores information
+like wiki pages, tickets, reports, settings, etc. A Trac environment consists
+of the environment configuration file (trac.ini), custom templates, log files,
+and more.
+
+A new Trac environment is created with trac-admin:
+
+  $ trac-admin /path/to/projectenv initenv
+
+Note: The user account under which the web server is run needs write permission
+to the environment directory and all the files inside.
+
+trac-admin will prompt you for the name of the project, where your subversion
+repository is located, what database you want to use, etc.
+
+
+Running the Standalone Server
+-----------------------------
+After having created a Trac environment, you can easily try the web interface
+by running the standalone server tracd:
+
+  $ tracd --port 8000 /path/to/projectenv
+
+Then, fire up a browser and visit http://localhost:8000/. You should get a
+simple listing of all environments that tracd knows about. Follow the link
+to the environment you just created, and you should see Trac in action.
+
+
+Running Trac on a Web Server
+----------------------------
+Trac provides three options for connecting to a "real" web server: CGI, FastCGI
+and mod_python. For decent performance, it is recommended that you use either
+FastCGI or mod_python.
+
+Please refer to the TracInstall page for details on these setups. You can find
+it either in the wiki of the Trac project you just created, or on the main Trac
+site.
+
+
+Using Trac
+----------
+Once you have your Trac site up and running, you should be able to browse your
+subversion repository, create tickets, view the timeline, etc.
+
+Keep in mind that anonymous (not logged in) users can by default access most
+but not all of the features. You will need to configure authentication and
+grant additional permissions to authenticated users to see the full set of
+features.
+
+For further documentation, see the TracGuide wiki page.
+
+Enjoy!
+
+/The Trac Team
+
+Please also consider joining the mailing list at
+<http://lists.edgewall.com/mailman/listinfo/trac/>.
+
+
+Visit the Trac open source project at <http://trac.edgewall.com/>
new file mode 100644
--- /dev/null
+++ b/examples/trac/MANIFEST.in
@@ -0,0 +1,35 @@
+include AUTHORS ChangeLog COPYING INSTALL MANIFEST.in README README.tracd RELEASE setup.cfg THANKS UPGRADE
+include scripts/trac-admin
+include scripts/tracd
+include scripts/tracdb2env
+include scripts/trac-postinstall.py
+include scripts/*.1
+include scripts/rpm-install.sh
+include cgi-bin/trac.cgi
+include cgi-bin/trac.fcgi
+include trac/tests/*.py
+include trac/web/tests/*.py
+include trac/wiki/tests/*.py
+include trac/wiki/tests/wiki-tests.txt
+include trac/ticket/tests/*.py
+include trac/scripts/tests/*.py
+include trac/scripts/tests/admin-tests.txt
+include trac/versioncontrol/tests/*.py
+include trac/versioncontrol/tests/svnrepos.dump
+recursive-include htdocs *.png *.gif *.ico *.js *README
+recursive-include htdocs/css *.css
+recursive-include htdocs/js *.js
+recursive-include doc *trac_logo.* *README
+recursive-include wiki-default *
+recursive-include wiki-macros *.py
+recursive-include templates *.cs *README
+recursive-include contrib *
+global-exclude *~
+global-exclude *.tmp
+global-exclude *.bak
+global-exclude *.old
+global-exclude .svn
+global-exclude .svn/*
+global-exclude .svn/*/*
+global-exclude .svn/*/*/*
+exclude wiki-default/checkwiki.py*
new file mode 100644
--- /dev/null
+++ b/examples/trac/README
@@ -0,0 +1,24 @@
+About Trac
+==========
+
+Trac is a minimalistic web-based software project management and bug/issue
+tracking system. It provides an interface to the Subversion revision control
+systems, an integrated wiki, flexible issue tracking and convenient report
+facilities.
+
+Trac is distributed using the modified BSD License.
+
+ * For installation instructions, please see the INSTALL. *
+ * If you are upgrading from a previous Trac version, please read UPGRADE. *
+
+You might also want to take a look at the RELEASE and ChangeLog files for more
+information.
+
+Otherwise, the primary source of information is the main Trac web site:
+
+ <http://trac.edgewall.com/>
+
+We hope you enjoy it,
+
+/The Trac Team
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/README.tracd
@@ -0,0 +1,50 @@
+Trac in stand-alone mode
+========================
+
+Trac 0.7 introduced among many other important features, the capability to run
+Trac as a stand-alone server (daemon), without a web server.
+
+Tracd supports all features of the CGI version (trac.cgi), 
+and can serve multiple projects from a single server instance.
+
+Running tracd
+-------------
+
+  tracd [options] <database> [database] ...
+
+  Options:
+
+-a, --auth <project,htdigest_file,realm>    Per-project authentication information
+-p, --port <port>                           Port number to use (default: 80)
+-b, --hostname <hostname>                   IP to bind to (default: '')
+
+
+Example 1: Single Project (non-authenticated)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+  $ tracd -p 9090 /var/trac/myproject
+
+Example 2: Multiple Projects (authenticated)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+  $ tracd -p 9090 \
+     -a projectA,/var/trac/htdigest.ALPHA,ALPHA  \
+     -a projectB,/var/trac/htdigest.ALPHA,ALPHA  \
+     /var/trac/projectA \
+     /var/trac/projectB
+
+The file ``htdigest.ALPHA`` can be generated using the Apache2 tool ``htdigest``
+(be carefull *NOT* to use ``htpasswd`` here).
+
+Feedback and bug reports
+------------------------
+
+Please provide feedback on tracd using the issue tracker or the mailing list.
+
+ Submit a bug report:  http://projects.edgewall.com/trac/newticket?component=tracd
+   Mailing list info:  http://projects.edgewall.com/trac/wiki/MailingList
+
+
+Thanks,
+
+/The Trac Team (http://trac.edgewall.com/)
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/RELEASE
@@ -0,0 +1,66 @@
+Trac 0.9.5 Release Notes
+============================
+April 18, 2006
+
+We're proud to present our latest release - Trac 0.9.5.
+
+Trac is an enhanced wiki and issue tracking system, integrated with
+Subversion, for software development projects. Trac uses a minimalistic
+approach to web-based software project management. Our mission is simple; to
+help developers write great software while staying out of the way. Trac should
+impose as little as possible on a team's established development process and
+policies.
+
+Trac allows wiki markup in issue descriptions and commit messages, creating
+links and useful structure between bugs, tasks, changesets, files and wiki
+pages.  A timeline view presents all project events in chronological order,
+making tracking progress or getting an overview of a project easy.
+
+For more information, please visit the main Trac web site:
+
+  <http://trac.edgewall.com/>
+
+The software, published under the modified BSD License, is
+available at:
+
+  <http://projects.edgewall.com/trac/download/>
+
+Please report problems and provide feedback in the project issue tracker:
+
+  <http://projects.edgewall.com/trac/>
+
+For questions, comments and user discussions, please use the Trac mailing list.
+List information, subscription and archive available at:
+
+  <http://projects.edgewall.com/trac/wiki/MailingList>
+
+
+What's New
+----------
+A brief summary of major changes for version 0.9.5:
+
+ * Fixed wiki macro XSS vulnerability.
+ * Smaller memory usage when accessing subversion history.
+ * Fixed issue with incorrectly generated urls when installed behind a web 
+   proxy.
+
+For a more complete list of improvements, see the ChangeLog at:
+
+ <http://projects.edgewall.com/trac/wiki/ChangeLog>
+
+
+Acknowledgements
+----------------
+Many thanks to the growing number of people who have, and continue to,
+support the project. Also our thanks to all people providing feedback and bug
+reports that helps us make Trac better, easier to use and more effective.
+
+Without your invaluable help, Trac would not evolve. Thank you all.
+
+Finally, we offer hope that Trac will prove itself useful to like-minded
+programmers around the world, and that this release will prove an improvement
+over the last version.
+
+Please let us know. :-)
+
+/The Trac Team <http://trac.edgewall.com/>
new file mode 100644
--- /dev/null
+++ b/examples/trac/THANKS
@@ -0,0 +1,68 @@
+ * Brad Anderson                  brad@dsource.org
+ * Christopher Armstrong          radix
+ * Jani Averbach                  jaa@jaa.iki.fi
+ * Juanma Barranquero             lektu@terra.es
+ * Christian Boos                 cboos@bct-technology.com
+ * Rocky Burt                     rocky.burt@myrealbox.com
+ * Toni Brkic                     toni.brkic@switchcore.com
+ * Felix Colins                   felix@keyghost.com
+ * Wesley Crucius                 wcrucius@sandc.com
+ * dju'                           
+ * Daragh Fitzpatrick             Daragh@i2i-Tech.com
+ * Markus Fuchs                   
+ * Eric Gillespie                 epg@netbsd.org
+ * Matthew Good                   trac@matt-good.net
+ * Shun-ichi Goto                 gotoh@taiyo.co.jp
+ * Chris Green                    cmgreen@uab.edu
+ * Mikael Hallendal               micke@imendio.com
+ * Stephen Hansen                 shansen@advpubtech.com
+ * Laurie Harper                  zodiac@holoweb.net
+ * Francois Harvey                fharvey@securiweb.net
+ * Tim Hatch                      trac@timhatch.com
+ * Michael Hope                   michael.hope@hamjet.co.nz
+ * Richard Hult                   richard@imendio.com
+ * Nuutti Kotivuori               naked@iki.fi
+ * Ian Leader                     ian.leader@line.co.uk
+ * Christopher Lenz               cmlenz@gmx.de
+ * Ivo Looser                     ivo.looser@gmail.com
+ * Rui Lopes                      rgl ruilopes com
+ * Angel Marin                    anmar@gmx.net
+ * Keir Mierle                    keir@cs.utoronto.ca
+ * James Moger                    jamesm@transonic.com
+ * Tim Moloney                    moloney@mrsl.com
+ * Jennifer Murtell               jen@jmurtell.com
+ * Jacob Norda                    jacobnorda@gmail.com
+ * Jeroen Ruigrok van der Werven  asmodai@in-nomine.org
+ * Juracy Filho                   juracy@gmail.com
+ * Cap Petschulat                 cap@cdres.com
+ * Nicholas Riley                 sabi
+ * Manuzhai                       manuzhai@gmail.com
+ * Mark Rowe                      mark.rowe@bdash.net.nz
+ * Olliver Rutherfurd             ollie
+ * pkou                           pkou@ua.fm
+ * Andres Salomon                 dilinger@athenacr.com
+ * Michael Scherer                misc@mandrake.org
+ * Andreas Schrattenecker         vittorio
+ * Emmeran Seehuber               rototor@rototor.de
+ * Noah Slater                    nslater@gmail.com
+ * Bill Soudan                    bill@soudan.net
+ * Ludvig Strigeus                
+ * Alec Thomas                    alec@swapoff.org
+ * Jani Tiainen                   redetin@luukku.com
+ * Zilvinas Valinskas             zilvinas@gemtek.lt
+ * Jason Vasquez                  jason@mugfu.com
+ * Jeff Weiss                     trac@jeffweiss.org
+ * Dmitry Yusupov                 dmitry_yus@yahoo.com
+
+The ever so elusive Anonymous.
+
+`Diggs and Lula`_ (official pawprint contributors)
+
+And everyone who keeps sending feedback, helping us improve Trac.
+
+----
+
+Our apologies to everyone we forgot to mention, but without whose invaluable
+help, Trac would not continue to rapidly evolve.
+
+.. _Diggs and Lula: http://people.edgewall.com/~daniel/lula_diggs.jpg
new file mode 100644
--- /dev/null
+++ b/examples/trac/UPGRADE
@@ -0,0 +1,168 @@
+Upgrade Instructions
+====================
+
+A Trac environment sometimes needs to be upgraded before it can be used with
+a new version of Trac. This document describes the steps necessary to upgrade
+an environment.
+
+Note that Environment upgrades are not necessary for minor version releases
+unless otherwise noted. For example, there's no need to upgrade a Trac
+environment created with (or upgraded) 0.8.0 when installing 0.8.4 (or any
+other 0.8.x release).
+
+General Instructions
+--------------------
+Typically, there are four steps involved in upgrading to a newer version of
+Trac:
+
+1. Update the Trac Code
+
+Get the new version of Trac, either by downloading an offical release package
+or by checking it out from the Subversion repository.
+
+If you have a source distribution, you need to run
+
+   python setup.py install
+
+to install the new version. If you've downloaded the Windows installer, you
+execute it, and so on.
+
+In any case, if you're doing a major version upgrade (such as from 0.8 to
+0.9), it is highly recommended that you first remove the existing Trac code.
+To do this, you need to delete the `trac` directory from the Python
+`lib/site-packages` directory. You may also want to remove the Trac `cgi-bin`,
+`htdocs` and `templates` directories that are commonly found in a directory
+called `share/trac` (the exact location depends on your platform).
+
+2. Upgrade the Trac Environment
+
+Unless noted otherwise, upgrading between major versions (such as 0.8 and
+0.9) involves changes to the database schema, and possibly the layout of the
+environment. Fortunately, Trac provides automated upgrade scripts to ease the
+pain. These scripts are run via `trac-admin`:
+
+   trac-admin /path/to/projenv upgrade
+
+This command will do nothing if the environment is already up-to-date.
+
+3. Update the Trac Documentation
+
+Every Trac environment includes a copy of the Trac documentation for the
+installed version. As you probably want to keep the included documentation in
+sync with the installed version of Trac, `trac-admin` provides a command to
+upgrade the documentation:
+
+   trac-admin /path/to/projenv wiki upgrade
+
+Note that this procedure will of course leave your `WikiStart` page intact.
+
+4. Restart the Web Server
+
+In order to reload the new Trac code you will need to restart your web
+server (note this is not necessary for CGI).
+
+
+The following sections discuss any extra actions that may need to be taken
+to upgrade to specific versions of Trac.
+
+
+From 0.9-beta to 0.9
+--------------------
+
+If inclusion of the static resources (style sheets, javascript, images) is not
+working, check the value of the `htdocs_location` in trac.ini. For mod_python,
+Tracd and FastCGI, you can simply remove the option altogether. For CGI, you
+should fix it to point to the URL you mapped the Trac `htdocs` directory to.
+
+If you've been using plugins with a beta release of Trac 0.9, or have 
+disabled some of the built-in components, you might have to update the rules
+for disabling/enabling components in trac.ini. In particular, globally 
+installed plugins now need to be enabled explicitly. See the TracPlugins and 
+TracIni wiki pages for more information.
+
+If you want to enable the display of all ticket changes in the timeline (the
+Ticket Details option), you now have to explicitly enable that in trac.ini,
+too:
+
+   [timeline]
+   ticket_show_details = true
+
+
+From 0.8.x to 0.9
+-----------------
+
+mod_python users will need to change the name of the mod_python handler in
+the Apache HTTPD configuration:
+
+   from: PythonHandler trac.ModPythonHandler
+   to:   PythonHandler trac.web.modpython_frontend
+
+If you have PySQLite 2.x installed, Trac will now try to open your SQLite
+database using the SQLite 3.x file format. The database formats used by
+SQLite 2.8.x and SQLite 3.x are incompatible. If you get an error like "file
+is encrypted or is not a database" after upgrading, then you must convert
+your database file.
+
+To do this, you need to have both SQLite 2.8.x and SQLite 3.x installed (they
+have different filenames so can coexist on the same system). Then use the
+following commands:
+
+   mv trac.db trac2.db
+   sqlite trac2.db .dump | sqlite3 trac.db
+
+After testing that the conversion was successful, the `trac2.db` file can be
+deleted. For more information on the SQLite upgrade see
+http://www.sqlite.org/version3.html.
+
+
+From 0.7.x to 0.8
+-----------------
+
+0.8 adds a new roadmap feature which requires additional permissions. While a
+fresh installation will by default grant `ROADMAP_VIEW` and `MILESTONE_VIEW`
+permissions to anonymous, these permissions have to be granted manually when
+upgrading:
+
+   trac-admin /path/to/projectenv permission add anonymous MILESTONE_VIEW
+   trac-admin /path/to/projectenv permission add anonymous ROADMAP_VIEW
+
+
+From 0.6.x to 0.7
+-----------------
+Trac 0.7 introduced a new database format, requiring manual upgrade.
+
+Previous versions of Trac stored wiki pages, ticket, reports, settings,
+etc. in a single SQLite database file. Trac 0.7 replaces this file
+with a new backend storage format; the 'Trac Environment', which is a
+directory containing an SQLite database, a human-readable configuration file,
+log-files and attachments.
+
+Fear not though, old-style Trac databases can easily be converted to
+Environments using the included `tracdb2env` program as follows:
+
+   tracdb2env /path/to/old/project.db /path/to/new/projectenv
+
+`tracdb2env` will create a new environment and copy the information from the
+old database to the new environment. The existing database will not be
+modified.
+
+You also need to update your apache configuration:
+
+Change the line:
+
+   SetEnv TRAC_DB "/path/to/old/project.db"
+
+to:
+
+   SetEnv TRAC_ENV "/path/to/new/projectenv"
+
+
+----
+
+If you have trouble upgrading Trac, please ask questions on the mailing list:
+
+  <http://projects.edgewall.com/trac/wiki/MailingList>
+
+Or for other support options, see:
+
+  <http://projects.edgewall.com/trac/wiki/TracSupport>
new file mode 100755
--- /dev/null
+++ b/examples/trac/cgi-bin/trac.cgi
@@ -0,0 +1,36 @@
+#!/usr/bin/python
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2003-2004 Edgewall Software
+# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+
+try:
+    from trac.web import cgi_frontend
+    cgi_frontend.run()
+
+except Exception, e:
+    import sys
+    import traceback
+
+    print>>sys.stderr, e
+    traceback.print_exc(file=sys.stderr)
+
+    print 'Status: 500 Internal Server Error'
+    print 'Content-Type: text/plain'
+    print
+    print 'Oops...'
+    print
+    print 'Trac detected an internal error:', e
+    print
+    traceback.print_exc(file=sys.stdout)
new file mode 100755
--- /dev/null
+++ b/examples/trac/cgi-bin/trac.fcgi
@@ -0,0 +1,33 @@
+#!/usr/bin/python
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2003-2004 Edgewall Software
+# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+
+try:
+    from trac.web import fcgi_frontend
+    fcgi_frontend.run()
+except Exception, e:
+    print 'Content-Type: text/plain\r\n\r\n',
+    print 'Oops...'
+    print
+    print 'Trac detected an internal error:'
+    print
+    print e
+    print
+    import traceback
+    import StringIO
+    tb = StringIO.StringIO()
+    traceback.print_exc(file=tb)
+    print tb.getvalue()
new file mode 100644
--- /dev/null
+++ b/examples/trac/contrib/README
@@ -0,0 +1,7 @@
+This directory contains useful contributed scripts and programs for Trac.
+
+Please note that these scripts are provided AS-IS are NOT tested to the same
+extent as the main sources. They are usually maintained by the original
+author, typically listed at the top of the source file.
+
+See ../THANKS for a list of contributors.
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/contrib/bugzilla2trac.py
@@ -0,0 +1,883 @@
+#!/usr/bin/env python
+
+"""
+Import a Bugzilla items into a Trac database.
+
+Requires:  Trac 0.9b1 from http://trac.edgewall.com/
+           Python 2.3 from http://www.python.org/
+           MySQL >= 3.23 from http://www.mysql.org/
+
+Thanks:    Mark Rowe <mrowe@bluewire.net.nz> 
+            for original TracDatabase class
+           
+Copyright 2004, Dmitry Yusupov <dmitry_yus@yahoo.com>
+
+Many enhancements, Bill Soudan <bill@soudan.net>
+Other enhancements, Florent Guillaume <fg@nuxeo.com>
+Reworked, Jeroen Ruigrok van der Werven <asmodai@tendra.org>
+
+$Id$
+"""
+
+import re
+
+###
+### Conversion Settings -- edit these before running if desired
+###
+
+# Bugzilla version.  You can find this in Bugzilla's globals.pl file.
+#
+# Currently, the following bugzilla versions are known to work:
+#   2.11 (2110), 2.16.5 (2165), 2.18.3 (2183), 2.19.1 (2191)
+#
+# If you run this script on a version not listed here and it is successful,
+# please report it to the Trac mailing list and drop a note to
+# asmodai@tendra.org so we can update the list.
+BZ_VERSION = 2180
+
+# MySQL connection parameters for the Bugzilla database.  These can also 
+# be specified on the command line.
+BZ_DB = ""
+BZ_HOST = ""
+BZ_USER = ""
+BZ_PASSWORD = ""
+
+# Path to the Trac environment.
+TRAC_ENV = "/usr/local/trac"
+
+# If true, all existing Trac tickets and attachments will be removed 
+# prior to import.
+TRAC_CLEAN = True
+
+# Enclose imported ticket description and comments in a {{{ }}} 
+# preformat block?  This formats the text in a fixed-point font.
+PREFORMAT_COMMENTS = False
+
+# Replace bug numbers in comments with #xyz
+REPLACE_BUG_NO = False
+
+# Severities
+SEVERITIES = [
+    ("blocker",  "1"),
+    ("critical", "2"),
+    ("major",    "3"),
+    ("normal",   "4"),
+    ("minor",    "5"),
+    ("trivial",  "6")
+]
+
+# Priorities
+# If using the default Bugzilla priorities of P1 - P5, do not change anything
+# here.
+# If you have other priorities defined please change the P1 - P5 mapping to
+# the order you want.  You can also collapse multiple priorities on bugzilla's
+# side into the same priority on Trac's side, simply adjust PRIORITIES_MAP.
+PRIORITIES = [
+    ("highest", "1"),
+    ("high",    "2"),
+    ("normal",  "3"),
+    ("low",     "4"),
+    ("lowest",  "5")
+]
+
+# Bugzilla: Trac
+# NOTE: Use lowercase.
+PRIORITIES_MAP = {
+    "p1": "highest",
+    "p2": "high",
+    "p3": "normal",
+    "p4": "low",
+    "p5": "lowest"
+}
+
+# By default, all bugs are imported from Bugzilla.  If you add a list
+# of products here, only bugs from those products will be imported.
+PRODUCTS = []
+# These Bugzilla products will be ignored during import.
+IGNORE_PRODUCTS = []
+
+# These milestones are ignored
+IGNORE_MILESTONES = ["---"]
+
+# These logins are converted to these user ids
+LOGIN_MAP = {
+    #'some.user@example.com': 'someuser',
+}
+
+# These emails are removed from CC list
+IGNORE_CC = [
+    #'loser@example.com',
+]
+
+# The 'component' field in Trac can come either from the Product or
+# or from the Component field of Bugzilla. COMPONENTS_FROM_PRODUCTS
+# switches the behavior.
+# If COMPONENTS_FROM_PRODUCTS is True:
+# - Bugzilla Product -> Trac Component
+# - Bugzilla Component -> Trac Keyword
+# IF COMPONENTS_FROM_PRODUCTS is False:
+# - Bugzilla Product -> Trac Keyword
+# - Bugzilla Component -> Trac Component
+COMPONENTS_FROM_PRODUCTS = False
+
+# If COMPONENTS_FROM_PRODUCTS is True, the default owner for each
+# Trac component is inferred from a default Bugzilla component.
+DEFAULT_COMPONENTS = ["default", "misc", "main"]
+
+# This mapping can assign keywords in the ticket entry to represent
+# products or components (depending on COMPONENTS_FROM_PRODUCTS).
+# The keyword will be ignored if empty.
+KEYWORDS_MAPPING = {
+    #'Bugzilla_product_or_component': 'Keyword',
+    "default": "",
+    "misc": "",
+    }
+
+# If this is True, products or components are all set as keywords
+# even if not mentionned in KEYWORDS_MAPPING.
+MAP_ALL_KEYWORDS = True
+
+
+# Bug comments that should not be imported.  Each entry in list should
+# be a regular expression.
+IGNORE_COMMENTS = [
+   "^Created an attachment \(id="
+]
+
+###########################################################################
+### You probably don't need to change any configuration past this line. ###
+###########################################################################
+
+# Bugzilla status to Trac status translation map.
+#
+# NOTE: bug activity is translated as well, which may cause bug
+# activity to be deleted (e.g. resolved -> closed in Bugzilla
+# would translate into closed -> closed in Trac, so we just ignore the
+# change).
+#
+# There is some special magic for open in the code:  if there is no
+# Bugzilla owner, open is mapped to 'new' instead.
+STATUS_TRANSLATE = {
+  "unconfirmed": "new",
+  "open":        "assigned",
+  "resolved":    "closed",
+  "verified":    "closed",
+  "released":    "closed"
+}
+
+# Translate Bugzilla statuses into Trac keywords.  This provides a way 
+# to retain the Bugzilla statuses in Trac.  e.g. when a bug is marked 
+# 'verified' in Bugzilla it will be assigned a VERIFIED keyword.
+STATUS_KEYWORDS = {
+  "verified": "VERIFIED",
+  "released": "RELEASED"
+}
+
+# Some fields in Bugzilla do not have equivalents in Trac.  Changes in
+# fields listed here will not be imported into the ticket change history,
+# otherwise you'd see changes for fields that don't exist in Trac.
+IGNORED_ACTIVITY_FIELDS = ["everconfirmed"]
+
+# Regular expression and its replacement
+BUG_NO_RE = re.compile(r"\b(bug #?)([0-9])")
+BUG_NO_REPL = r"#\2"
+
+###
+### Script begins here
+###
+
+import os
+import sys
+import string
+import StringIO
+
+import MySQLdb
+import MySQLdb.cursors
+try:
+    from trac.env import Environment
+except:
+    from trac.Environment import Environment
+from trac.attachment import Attachment
+
+if not hasattr(sys, 'setdefaultencoding'):
+    reload(sys)
+
+sys.setdefaultencoding('latin1')
+
+# simulated Attachment class for trac.add
+#class Attachment:
+#    def __init__(self, name, data):
+#        self.filename = name
+#        self.file = StringIO.StringIO(data.tostring())
+  
+# simple field translation mapping.  if string not in
+# mapping, just return string, otherwise return value
+class FieldTranslator(dict):
+    def __getitem__(self, item):
+        if not dict.has_key(self, item):
+            return item
+            
+        return dict.__getitem__(self, item)
+
+statusXlator = FieldTranslator(STATUS_TRANSLATE)
+
+class TracDatabase(object):
+    def __init__(self, path):
+        self.env = Environment(path)
+        self._db = self.env.get_db_cnx()
+        self._db.autocommit = False
+        self.loginNameCache = {}
+        self.fieldNameCache = {}
+    
+    def db(self):
+        return self._db
+    
+    def hasTickets(self):
+        c = self.db().cursor()
+        c.execute("SELECT count(*) FROM Ticket")
+        return int(c.fetchall()[0][0]) > 0
+
+    def assertNoTickets(self):
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+    
+    def setSeverityList(self, s):
+        """Remove all severities, set them to `s`"""
+        self.assertNoTickets()
+        
+        c = self.db().cursor()
+        c.execute("DELETE FROM enum WHERE type='severity'")
+        for value, i in s:
+            print "  inserting severity '%s' - '%s'" % (value, i)
+            c.execute("""INSERT INTO enum (type, name, value)
+                                   VALUES (%s, %s, %s)""",
+                      ("severity", value.encode('utf-8'), i))
+        self.db().commit()
+    
+    def setPriorityList(self, s):
+        """Remove all priorities, set them to `s`"""
+        self.assertNoTickets()
+        
+        c = self.db().cursor()
+        c.execute("DELETE FROM enum WHERE type='priority'")
+        for value, i in s:
+            print "  inserting priority '%s' - '%s'" % (value, i)
+            c.execute("""INSERT INTO enum (type, name, value)
+                                   VALUES (%s, %s, %s)""",
+                      ("priority", value.encode('utf-8'), i))
+        self.db().commit()
+
+    
+    def setComponentList(self, l, key):
+        """Remove all components, set them to `l`"""
+        self.assertNoTickets()
+        
+        c = self.db().cursor()
+        c.execute("DELETE FROM component")
+        for comp in l:
+            print "  inserting component '%s', owner '%s'" % \
+                            (comp[key], comp['owner'])
+            c.execute("INSERT INTO component (name, owner) VALUES (%s, %s)",
+                      (comp[key].encode('utf-8'),
+                       comp['owner'].encode('utf-8')))
+        self.db().commit()
+    
+    def setVersionList(self, v, key):
+        """Remove all versions, set them to `v`"""
+        self.assertNoTickets()
+        
+        c = self.db().cursor()
+        c.execute("DELETE FROM version")
+        for vers in v:
+            print "  inserting version '%s'" % (vers[key])
+            c.execute("INSERT INTO version (name) VALUES (%s)",
+                      (vers[key].encode('utf-8'),))
+        self.db().commit()
+        
+    def setMilestoneList(self, m, key):
+        """Remove all milestones, set them to `m`"""
+        self.assertNoTickets()
+        
+        c = self.db().cursor()
+        c.execute("DELETE FROM milestone")
+        for ms in m:
+            milestone = ms[key]
+            print "  inserting milestone '%s'" % (milestone)
+            c.execute("INSERT INTO milestone (name) VALUES (%s)",
+                      (milestone.encode('utf-8'),))
+        self.db().commit()
+    
+    def addTicket(self, id, time, changetime, component, severity, priority,
+                  owner, reporter, cc, version, milestone, status, resolution,
+                  summary, description, keywords):
+        c = self.db().cursor()
+        
+        desc = description.encode('utf-8')
+        type = "defect"
+        
+        if severity.lower() == "enhancement":
+                severity = "minor"
+                type = "enhancement"
+        
+        if PREFORMAT_COMMENTS:
+          desc = '{{{\n%s\n}}}' % desc
+
+        if REPLACE_BUG_NO:
+            if BUG_NO_RE.search(desc):
+                desc = re.sub(BUG_NO_RE, BUG_NO_REPL, desc)
+
+        if PRIORITIES_MAP.has_key(priority):
+            priority = PRIORITIES_MAP[priority]
+
+        print "  inserting ticket %s -- %s" % (id, summary)
+
+        c.execute("""INSERT INTO ticket (id, type, time, changetime, component,
+                                         severity, priority, owner, reporter,
+                                         cc, version, milestone, status,
+                                         resolution, summary, description,
+                                         keywords)
+                                 VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s,
+                                         %s, %s, %s, %s, %s, %s, %s, %s)""",
+                  (id, type.encode('utf-8'), time.strftime('%s'),
+                   changetime.strftime('%s'), component.encode('utf-8'),
+                   severity.encode('utf-8'), priority.encode('utf-8'), owner,
+                   reporter, cc, version, milestone.encode('utf-8'),
+                   status.lower(), resolution, summary.encode('utf-8'), desc,
+                   keywords))
+        
+        self.db().commit()
+        return self.db().get_last_id(c, 'ticket')
+    
+    def addTicketComment(self, ticket, time, author, value):
+        comment = value.encode('utf-8')
+        
+        if PREFORMAT_COMMENTS:
+          comment = '{{{\n%s\n}}}' % comment
+
+        if REPLACE_BUG_NO:
+            if BUG_NO_RE.search(comment):
+                comment = re.sub(BUG_NO_RE, BUG_NO_REPL, comment)
+
+        c = self.db().cursor()
+        c.execute("""INSERT INTO ticket_change (ticket, time, author, field,
+                                                oldvalue, newvalue)
+                                        VALUES (%s, %s, %s, %s, %s, %s)""",
+                  (ticket, time.strftime('%s'), author, 'comment', '', comment))
+        self.db().commit()
+
+    def addTicketChange(self, ticket, time, author, field, oldvalue, newvalue):
+        c = self.db().cursor()
+
+        if field == "owner":
+            if LOGIN_MAP.has_key(oldvalue):
+                oldvalue = LOGIN_MAP[oldvalue]
+            if LOGIN_MAP.has_key(newvalue):
+                newvalue = LOGIN_MAP[newvalue]
+
+        if field == "priority":
+            if PRIORITIES_MAP.has_key(oldvalue.lower()):
+                oldvalue = PRIORITIES_MAP[oldvalue.lower()]
+            if PRIORITIES_MAP.has_key(newvalue.lower()):
+                newvalue = PRIORITIES_MAP[newvalue.lower()]
+
+        # Doesn't make sense if we go from highest -> highest, for example.
+        if oldvalue == newvalue:
+            return
+        
+        c.execute("""INSERT INTO ticket_change (ticket, time, author, field,
+                                                oldvalue, newvalue)
+                                        VALUES (%s, %s, %s, %s, %s, %s)""",
+                  (ticket, time.strftime('%s'), author, field,
+                   oldvalue.encode('utf-8'), newvalue.encode('utf-8')))
+        self.db().commit()
+        
+    def addAttachment(self, author, a):
+        description = a['description'].encode('utf-8')
+        id = a['bug_id']
+        filename = a['filename'].encode('utf-8')
+        filedata = StringIO.StringIO(a['thedata'].tostring())
+        filesize = len(filedata.getvalue())
+        time = a['creation_ts']
+        print "    ->inserting attachment '%s' for ticket %s -- %s" % \
+                (filename, id, description)
+
+        attachment = Attachment(self.env, 'ticket', id)
+        attachment.author = author
+        attachment.description = description
+        attachment.insert(filename, filedata, filesize, time.strftime('%s'))
+        del attachment
+        
+    def getLoginName(self, cursor, userid):
+        if userid not in self.loginNameCache:
+            cursor.execute("SELECT * FROM profiles WHERE userid = %s", (userid))
+            loginName = cursor.fetchall()
+
+            if loginName:
+                loginName = loginName[0]['login_name']
+            else:
+                print """WARNING: unknown bugzilla userid %d, recording as
+                         anonymous""" % (userid)
+                loginName = "anonymous"
+
+            loginName = LOGIN_MAP.get(loginName, loginName)
+
+            self.loginNameCache[userid] = loginName
+
+        return self.loginNameCache[userid]
+
+    def getFieldName(self, cursor, fieldid):
+        if fieldid not in self.fieldNameCache:
+            cursor.execute("SELECT * FROM fielddefs WHERE fieldid = %s",
+                           (fieldid))
+            fieldName = cursor.fetchall()
+
+            if fieldName:
+                fieldName = fieldName[0]['name'].lower()
+            else:
+                print "WARNING: unknown bugzilla fieldid %d, \
+                                recording as unknown" % (userid)
+                fieldName = "unknown"
+
+            self.fieldNameCache[fieldid] = fieldName
+
+        return self.fieldNameCache[fieldid]
+
+def makeWhereClause(fieldName, values, negative=False):
+    if not values:
+        return ''
+    if negative:
+        connector, op = ' AND ', '!='
+    else:
+        connector, op = ' OR ', '='
+    clause = connector.join(["%s %s '%s'" % (fieldName, op, value) for value in values])
+    return ' ' + clause
+
+def convert(_db, _host, _user, _password, _env, _force):
+    activityFields = FieldTranslator()
+
+    # account for older versions of bugzilla
+    print "Using Bugzilla v%s schema." % BZ_VERSION
+    if BZ_VERSION == 2110:
+        activityFields['removed'] = "oldvalue"
+        activityFields['added'] = "newvalue"
+
+    # init Bugzilla environment
+    print "Bugzilla MySQL('%s':'%s':'%s':'%s'): connecting..." % \
+            (_db, _host, _user, ("*" * len(_password)))
+    mysql_con = MySQLdb.connect(host=_host, 
+                user=_user, passwd=_password, db=_db, compress=1, 
+                cursorclass=MySQLdb.cursors.DictCursor)
+    mysql_cur = mysql_con.cursor()
+
+    # init Trac environment
+    print "Trac SQLite('%s'): connecting..." % (_env)
+    trac = TracDatabase(_env)
+
+    # force mode...
+    if _force == 1:
+        print "\nCleaning all tickets..."
+        c = trac.db().cursor()
+        c.execute("DELETE FROM ticket_change")
+        trac.db().commit()
+        
+        c.execute("DELETE FROM ticket")
+        trac.db().commit()
+        
+        c.execute("DELETE FROM attachment")
+	attachments_dir = os.path.join(os.path.normpath(trac.env.path),
+                                "attachments")
+        # Straight from the Python documentation.
+        for root, dirs, files in os.walk(attachments_dir, topdown=False):
+            for name in files:
+                os.remove(os.path.join(root, name))
+            for name in dirs:
+                os.rmdir(os.path.join(root, name))
+        if not os.stat(attachments_dir):
+            os.mkdir(attachments_dir)
+        trac.db().commit()
+        print "All tickets cleaned..."
+
+
+    print "\n0. Filtering products..."
+    mysql_cur.execute("SELECT name FROM products")
+    products = []
+    for line in mysql_cur.fetchall():
+        product = line['name']
+        if PRODUCTS and product not in PRODUCTS:
+            continue
+        if product in IGNORE_PRODUCTS:
+            continue
+        products.append(product)
+    PRODUCTS[:] = products
+    print "  Using products", " ".join(PRODUCTS)
+
+    print "\n1. Import severities..."
+    trac.setSeverityList(SEVERITIES)
+
+    print "\n2. Import components..."
+    if not COMPONENTS_FROM_PRODUCTS:
+    	if BZ_VERSION >= 2180:
+	    sql = """SELECT DISTINCT c.name AS name, c.initialowner AS owner
+                                FROM components AS c, products AS p
+	                       WHERE c.product_id = p.id AND"""
+	    sql += makeWhereClause('p.name', PRODUCTS)
+	else:
+	    sql = "SELECT value AS name, initialowner AS owner FROM components"
+            sql += " WHERE" + makeWhereClause('program', PRODUCTS)
+        mysql_cur.execute(sql)
+        components = mysql_cur.fetchall()
+        for component in components:
+            component['owner'] = trac.getLoginName(mysql_cur,
+                                                   component['owner'])
+        trac.setComponentList(components, 'name')
+    else:
+        sql = """SELECT program AS product, value AS comp, initialowner AS owner
+                   FROM components"""
+        sql += " WHERE" + makeWhereClause('program', PRODUCTS)
+        mysql_cur.execute(sql)
+        lines = mysql_cur.fetchall()
+        all_components = {} # product -> components
+        all_owners = {} # product, component -> owner
+        for line in lines:
+            product = line['product']
+            comp = line['comp']
+            owner = line['owner']
+            all_components.setdefault(product, []).append(comp)
+            all_owners[(product, comp)] = owner
+        component_list = []
+        for product, components in all_components.items():
+            # find best default owner
+            default = None
+            for comp in DEFAULT_COMPONENTS:
+                if comp in components:
+                    default = comp
+                    break
+            if default is None:
+                default = components[0]
+            owner = all_owners[(product, default)]
+            owner_name = trac.getLoginName(mysql_cur, owner)
+            component_list.append({'product': product, 'owner': owner_name})
+        trac.setComponentList(component_list, 'product')
+
+    print "\n3. Import priorities..."
+    trac.setPriorityList(PRIORITIES)
+
+    print "\n4. Import versions..."
+    if BZ_VERSION >= 2180:
+        sql = """SELECT DISTINCTROW versions.value AS value
+                               FROM products, versions"""
+	sql += " WHERE" + makeWhereClause('products.name', PRODUCTS)
+    else:
+    	sql = "SELECT DISTINCTROW value FROM versions"
+    	sql += " WHERE" + makeWhereClause('program', PRODUCTS)
+    mysql_cur.execute(sql)
+    versions = mysql_cur.fetchall()
+    trac.setVersionList(versions, 'value')
+
+    print "\n5. Import milestones..."
+    sql = "SELECT DISTINCT value FROM milestones"
+    sql += " WHERE" + makeWhereClause('value', IGNORE_MILESTONES, negative=True)
+    mysql_cur.execute(sql)
+    milestones = mysql_cur.fetchall()
+    trac.setMilestoneList(milestones, 'value')
+
+    print "\n6. Retrieving bugs..."
+    sql = """SELECT DISTINCT b.*, c.name AS component, p.name AS product
+                        FROM bugs AS b, components AS c, products AS p """
+    sql += " WHERE (" + makeWhereClause('p.name', PRODUCTS)
+    sql += ") AND b.product_id = p.id"
+    sql += " AND b.component_id = c.id"
+    sql += " ORDER BY b.bug_id"
+    mysql_cur.execute(sql)
+    bugs = mysql_cur.fetchall()
+
+    
+    print "\n7. Import bugs and bug activity..."
+    for bug in bugs:
+        bugid = bug['bug_id']
+        
+        ticket = {}
+        keywords = []
+        ticket['id'] = bugid
+        ticket['time'] = bug['creation_ts']
+        ticket['changetime'] = bug['delta_ts']
+        if COMPONENTS_FROM_PRODUCTS:
+            ticket['component'] = bug['product']
+        else:
+            ticket['component'] = bug['component']
+        ticket['severity'] = bug['bug_severity']
+        ticket['priority'] = bug['priority'].lower()
+
+        ticket['owner'] = trac.getLoginName(mysql_cur, bug['assigned_to'])
+        ticket['reporter'] = trac.getLoginName(mysql_cur, bug['reporter'])
+
+        mysql_cur.execute("SELECT * FROM cc WHERE bug_id = %s", bugid)
+        cc_records = mysql_cur.fetchall()
+        cc_list = []
+        for cc in cc_records:
+            cc_list.append(trac.getLoginName(mysql_cur, cc['who']))
+        cc_list = [cc for cc in cc_list if '@' in cc and cc not in IGNORE_CC]
+        ticket['cc'] = string.join(cc_list, ', ')
+
+        ticket['version'] = bug['version']
+
+        target_milestone = bug['target_milestone']
+        if target_milestone in IGNORE_MILESTONES:
+            target_milestone = ''
+        ticket['milestone'] = target_milestone
+
+        bug_status = bug['bug_status'].lower()
+        ticket['status'] = statusXlator[bug_status]
+        ticket['resolution'] = bug['resolution'].lower()
+
+        # a bit of extra work to do open tickets
+        if bug_status == 'open':
+            if owner != '':
+                ticket['status'] = 'assigned'
+            else:
+                ticket['status'] = 'new'
+
+        ticket['summary'] = bug['short_desc']
+
+        mysql_cur.execute("SELECT * FROM longdescs WHERE bug_id = %s" % bugid) 
+        longdescs = list(mysql_cur.fetchall())
+
+        # check for empty 'longdescs[0]' field...
+        if len(longdescs) == 0:
+            ticket['description'] = ''
+        else:
+            ticket['description'] = longdescs[0]['thetext']
+            del longdescs[0]
+
+        for desc in longdescs:
+            ignore = False
+            for comment in IGNORE_COMMENTS:
+                if re.match(comment, desc['thetext']):
+                    ignore = True
+                    
+            if ignore:
+                    continue
+            
+            trac.addTicketComment(ticket=bugid,
+                time = desc['bug_when'],
+                author=trac.getLoginName(mysql_cur, desc['who']),
+                value = desc['thetext'])
+
+        mysql_cur.execute("""SELECT * FROM bugs_activity WHERE bug_id = %s
+                           ORDER BY bug_when""" % bugid)
+        bugs_activity = mysql_cur.fetchall()
+        resolution = ''
+        ticketChanges = []
+        keywords = []
+        for activity in bugs_activity:
+            field_name = trac.getFieldName(mysql_cur, activity['fieldid']).lower()
+            
+            removed = activity[activityFields['removed']]
+            added = activity[activityFields['added']]
+
+            # statuses and resolutions are in lowercase in trac
+            if field_name == "resolution" or field_name == "bug_status":
+                removed = removed.lower()
+                added = added.lower()
+
+            # remember most recent resolution, we need this later
+            if field_name == "resolution":
+                resolution = added.lower()
+
+            add_keywords = []
+            remove_keywords = []
+
+            # convert bugzilla field names...
+            if field_name == "bug_severity":
+                field_name = "severity"
+            elif field_name == "assigned_to":
+                field_name = "owner"
+            elif field_name == "bug_status":
+                field_name = "status"
+                if removed in STATUS_KEYWORDS:
+                    remove_keywords.append(STATUS_KEYWORDS[removed])
+                if added in STATUS_KEYWORDS:
+                    add_keywords.append(STATUS_KEYWORDS[added])
+                added = statusXlator[added]
+                removed = statusXlator[removed]
+            elif field_name == "short_desc":
+                field_name = "summary"
+            elif field_name == "product" and COMPONENTS_FROM_PRODUCTS:
+                field_name = "component"
+            elif ((field_name == "product" and not COMPONENTS_FROM_PRODUCTS) or
+                  (field_name == "component" and COMPONENTS_FROM_PRODUCTS)):
+                if MAP_ALL_KEYWORDS or removed in KEYWORDS_MAPPING:
+                    kw = KEYWORDS_MAPPING.get(removed, removed)
+                    if kw:
+                        remove_keywords.append(kw)
+                if MAP_ALL_KEYWORDS or added in KEYWORDS_MAPPING:
+                    kw = KEYWORDS_MAPPING.get(added, added)
+                    if kw:
+                        add_keywords.append(kw)
+                if field_name == "component":
+                    # just keep the keyword change
+                    added = removed = ""
+            elif field_name == "target_milestone":
+                field_name = "milestone"
+                if added in IGNORE_MILESTONES:
+                    added = ""
+                if removed in IGNORE_MILESTONES:
+                    removed = ""
+
+            ticketChange = {}
+            ticketChange['ticket'] = bugid
+            ticketChange['time'] = activity['bug_when']
+            ticketChange['author'] = trac.getLoginName(mysql_cur,
+                                                       activity['who'])
+            ticketChange['field'] = field_name
+            ticketChange['oldvalue'] = removed
+            ticketChange['newvalue'] = added
+
+            if add_keywords or remove_keywords:
+                # ensure removed ones are in old
+                old_keywords = keywords + [kw for kw in remove_keywords if kw
+                                           not in keywords]
+                # remove from new
+                keywords = [kw for kw in keywords if kw not in remove_keywords]
+                # add to new
+                keywords += [kw for kw in add_keywords if kw not in keywords]
+                if old_keywords != keywords:
+                    ticketChangeKw = ticketChange.copy()
+                    ticketChangeKw['field'] = "keywords"
+                    ticketChangeKw['oldvalue'] = ' '.join(old_keywords)
+                    ticketChangeKw['newvalue'] = ' '.join(keywords)
+                    ticketChanges.append(ticketChangeKw)
+
+            if field_name in IGNORED_ACTIVITY_FIELDS:
+                continue
+
+            # Skip changes that have no effect (think translation!).
+            if added == removed:
+                continue
+                
+            # Bugzilla splits large summary changes into two records.
+            for oldChange in ticketChanges:
+              if (field_name == "summary"
+                  and oldChange['field'] == ticketChange['field'] 
+                  and oldChange['time'] == ticketChange['time'] 
+                  and oldChange['author'] == ticketChange['author']):
+                  oldChange['oldvalue'] += " " + ticketChange['oldvalue'] 
+                  oldChange['newvalue'] += " " + ticketChange['newvalue']
+                  break
+              # cc sometime appear in different activities with same time
+              if (field_name == "cc" \
+                  and oldChange['time'] == ticketChange['time']):
+                  oldChange['newvalue'] += ", " + ticketChange['newvalue']
+                  break
+            else:
+                ticketChanges.append (ticketChange)
+
+        for ticketChange in ticketChanges:
+            trac.addTicketChange (**ticketChange)
+
+        # For some reason, bugzilla v2.11 seems to clear the resolution
+        # when you mark a bug as closed.  Let's remember it and restore
+        # it if the ticket is closed but there's no resolution.
+        if not ticket['resolution'] and ticket['status'] == "closed":
+            ticket['resolution'] = resolution
+
+        bug_status = bug['bug_status']
+        if bug_status in STATUS_KEYWORDS:
+            kw = STATUS_KEYWORDS[bug_status]
+            if kw not in keywords:
+                keywords.append(kw)
+
+        product = bug['product']
+        if product in KEYWORDS_MAPPING and not COMPONENTS_FROM_PRODUCTS:
+            kw = KEYWORDS_MAPPING.get(product, product)
+            if kw and kw not in keywords:
+                keywords.append(kw)
+
+        component = bug['component']
+        if (COMPONENTS_FROM_PRODUCTS and \
+            (MAP_ALL_KEYWORDS or component in KEYWORDS_MAPPING)):
+            kw = KEYWORDS_MAPPING.get(component, component)
+            if kw and kw not in keywords:
+                keywords.append(kw)
+
+        ticket['keywords'] = string.join(keywords)                
+        ticketid = trac.addTicket(**ticket)
+
+        mysql_cur.execute("SELECT * FROM attachments WHERE bug_id = %s" % bugid)
+        attachments = mysql_cur.fetchall()
+        for a in attachments:
+            author = trac.getLoginName(mysql_cur, a['submitter_id'])
+            trac.addAttachment(author, a)
+            
+    print "\n8. Importing users and passwords..."
+    if BZ_VERSION >= 2180:
+        mysql_cur.execute("SELECT login_name, cryptpassword FROM profiles")
+        users = mysql_cur.fetchall()
+    htpasswd = file("htpasswd", 'w')
+    for user in users:
+        if LOGIN_MAP.has_key(user['login_name']):
+            login = LOGIN_MAP[user['login_name']]
+        else:
+            login = user['login_name']
+        htpasswd.write(login + ":" + user['cryptpassword'] + "\n")
+
+    htpasswd.close()
+    print "  Bugzilla users converted to htpasswd format, see 'htpasswd'."
+
+    print "\nAll tickets converted."
+
+def log(msg):
+    print "DEBUG: %s" % (msg)
+
+def usage():
+    print """bugzilla2trac - Imports a bug database from Bugzilla into Trac.
+
+Usage: bugzilla2trac.py [options]
+
+Available Options:
+  --db <MySQL dbname>              - Bugzilla's database name
+  --tracenv /path/to/trac/env      - Full path to Trac db environment
+  -h | --host <MySQL hostname>     - Bugzilla's DNS host name
+  -u | --user <MySQL username>     - Effective Bugzilla's database user
+  -p | --passwd <MySQL password>   - Bugzilla's user password
+  -c | --clean                     - Remove current Trac tickets before
+                                     importing
+  --help | help                    - This help info
+
+Additional configuration options can be defined directly in the script.
+"""
+    sys.exit(0)
+
+def main():
+    global BZ_DB, BZ_HOST, BZ_USER, BZ_PASSWORD, TRAC_ENV, TRAC_CLEAN
+    if len (sys.argv) > 1:
+    	if sys.argv[1] in ['--help','help'] or len(sys.argv) < 4:
+    	    usage()
+    	iter = 1
+    	while iter < len(sys.argv):
+    	    if sys.argv[iter] in ['--db'] and iter+1 < len(sys.argv):
+    	        BZ_DB = sys.argv[iter+1]
+    	        iter = iter + 1
+    	    elif sys.argv[iter] in ['-h', '--host'] and iter+1 < len(sys.argv):
+    	        BZ_HOST = sys.argv[iter+1]
+    	        iter = iter + 1
+    	    elif sys.argv[iter] in ['-u', '--user'] and iter+1 < len(sys.argv):
+    	        BZ_USER = sys.argv[iter+1]
+    	        iter = iter + 1
+    	    elif sys.argv[iter] in ['-p', '--passwd'] and iter+1 < len(sys.argv):
+    	        BZ_PASSWORD = sys.argv[iter+1]
+    	        iter = iter + 1
+    	    elif sys.argv[iter] in ['--tracenv'] and iter+1 < len(sys.argv):
+    	        TRAC_ENV = sys.argv[iter+1]
+    	        iter = iter + 1
+    	    elif sys.argv[iter] in ['-c', '--clean']:
+    	        TRAC_CLEAN = 1
+    	    else:
+    	        print "Error: unknown parameter: " + sys.argv[iter]
+    	        sys.exit(0)
+    	    iter = iter + 1
+        
+    convert(BZ_DB, BZ_HOST, BZ_USER, BZ_PASSWORD, TRAC_ENV, TRAC_CLEAN)
+
+if __name__ == '__main__':
+    main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/contrib/emailfilter.py
@@ -0,0 +1,64 @@
+#!/usr/bin/python
+"""
+emailfilter.py -- Email tickets to Trac.
+
+A simple MTA filter to create Trac tickets from inbound emails.
+
+Copyright 2005, Daniel Lundin <daniel@edgewall.com>
+Copyright 2005, Edgewall Software
+
+The scripts reads emails from stdin and inserts directly into a Trac database.
+MIME headers are mapped as follows:
+
+    * From: => Reporter
+    * Subject: => Summary
+    * Body => Description
+
+How to use
+----------
+ * Set TRAC_ENV_PATH to the path of your project's Trac environment
+ * Configure script as a mail (pipe) filter with your MTA
+    typically, this involves adding a line like this to /etc/aliases:
+       somename: |/path/to/email2trac.py
+    Check your MTA's documentation for specifics.
+
+Todo
+----
+  * Configure target database through env variable?
+  * Handle/discard HTML parts
+  * Attachment support
+"""
+
+TRAC_ENV_PATH = '/var/trac/test'
+
+import email
+import sys
+
+from trac.env import Environment
+from trac.ticket import Ticket
+
+
+class TicketEmailParser(object):
+
+    env = None
+
+    def __init__(self, env):
+        self.env = env
+
+    def parse(self, fp):
+        msg = email.message_from_file(fp)
+        tkt = Ticket(self.env)
+        tkt['status'] = 'new'
+        tkt['reporter'] = msg['from']
+        tkt['summary'] = msg['subject']
+        for part in msg.walk():
+            if part.get_content_type() == 'text/plain':
+                tkt['description'] = part.get_payload(decode=1).strip()
+
+        if tkt.values.get('description'):
+            tkt.insert()
+
+if __name__ == '__main__':
+    env = Environment(TRAC_ENV_PATH, create=0)
+    tktparser = TicketEmailParser(env)
+    tktparser.parse(sys.stdin)
new file mode 100644
--- /dev/null
+++ b/examples/trac/contrib/migrateticketmodel.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+# 
+# This script completely migrates a <= 0.8.x Trac environment to use the new
+# default ticket model introduced in Trac 0.9.
+# 
+# In particular, this means that the severity field is removed (or rather
+# disabled by removing all possible values), and the priority values are
+# changed to the more meaningful new defaults.
+# 
+# Make sure to make a backup of the Trac environment before running this!
+
+import os
+import sys
+
+from trac.env import open_environment
+from trac.ticket.model import Priority, Severity
+
+priority_mapping = {
+    'highest':  'blocker',
+    'high':     'critical',
+    'normal':   'major',
+    'low':      'minor',
+    'lowest':   'trivial'
+}
+
+def main():
+    if len(sys.argv) < 2:
+        print >> sys.stderr, 'usage: %s /path/to/projenv' \
+                             % os.path.basename(sys.argv[0])
+        sys.exit(2)
+
+    env = open_environment(sys.argv[1])
+    db = env.get_db_cnx()
+
+    for oldprio, newprio in priority_mapping.items():
+        priority = Priority(env, oldprio, db)
+        priority.name = newprio
+        priority.update(db)
+
+    for severity in list(Severity.select(env, db)):
+        severity.delete(db)
+
+    db.commit()
+
+if __name__ == '__main__':
+    main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/contrib/sourceforge2trac.py
@@ -0,0 +1,305 @@
+"""
+Import a Sourceforge project's tracker items into a Trac database.
+
+Requires:  Development version of Trac 0.7-pre from http://trac.edgewall.com/
+           ElementTree from effbot.org/zone/element.htm
+           Python 2.3 from http://www.python.org/
+           
+The Sourceforge tracker items can be exported from the 'Backup' page of the
+project admin section.
+
+Copyright 2004, Mark Rowe <mrowe@bluewire.net.nz>
+"""
+
+from elementtree.ElementTree import ElementTree
+from datetime import datetime
+import trac.env
+
+class FieldParser(object):
+    def __init__(self, e):
+        for field in e:
+            if field.get('name').endswith('date'):
+                setattr(self, field.get('name'), datetime.fromtimestamp(int(field.text)))
+            else:
+                setattr(self, field.get('name'), field.text)        
+
+class ArtifactHistoryItem(FieldParser):
+    def __repr__(self):
+        return '<ArtifactHistoryItem field_name=%r old_value=%r entrydate=%r mod_by=%r>' % (
+            self.field_name, self.old_value, self.entrydate, self.mod_by)
+
+class ArtifactMessage(FieldParser):
+    def __repr__(self):
+        return '<ArtifactMessage adddate=%r user_name=%r body=%r>' % (self.adddate, self.user_name, self.body)
+
+class Artifact(object):
+    def __init__(self, e):
+        self._history = []
+        self._messages = []
+        
+        for field in e:
+            if field.get('name') == 'artifact_history':
+                for h in field:
+                    self._history.append(ArtifactHistoryItem(h))
+            elif field.get('name') == 'artifact_messages':
+                for m in field:
+                    self._messages.append(ArtifactMessage(m))
+            else:
+                setattr(self, field.get('name'), field.text)
+    
+    def history(self):
+        """Returns the history items in reverse chronological order so that the "new value"
+           can easily be calculated based on the final value of the field, and the old value
+           of the items occuring before it.
+        """
+        history = [(h.entrydate, h) for h in self._history]
+        history.sort()
+        return [h[1] for h in history][::-1]
+    
+    def messages(self):
+        return self._messages[:]
+    
+    def __repr__(self):
+        return '<Artifact summary=%r artifact_type=%r category=%r status=%r>' % (self.summary, self.artifact_type, self.category, self.status)
+
+class ExportedProjectData(object):
+    def __init__(self, f):
+        self._artifacts = []
+        
+        root = ElementTree().parse(f)   
+        
+        for artifact in root.find('artifacts'):
+            self._artifacts.append(Artifact(artifact))
+    
+    def artifacts(self):
+        """Returns the artifacts in chronological order, so that they will be assigned numbers in sequence."""
+        artifacts = [(a.open_date, a) for a in self._artifacts]
+        artifacts.sort()
+        return [a[1] for a in artifacts]
+    
+    def featureRequests(self):
+        return [a for a in self._artifacts if a.artifact_type == 'Feature Requests']
+    
+    def bugs(self):
+        return [a for a in self._artifacts if a.artifact_type == 'Bugs']
+    
+    def categories(self):
+        """Returns all the category names that are used, in alphabetical order."""
+        c = {}
+        for a in self._artifacts:
+            c[a.category] = 1
+        
+        categories = c.keys()
+        categories.sort()
+        return categories
+    
+    def groups(self):
+        """Returns all the group names that are used, in alphabetical order."""
+        g = {}
+        for a in self._artifacts:
+            g[a.artifact_group_id] = 1
+        del g['None']
+        
+        groups = g.keys()
+        groups.sort()
+        return groups
+    
+    def artifactTypes(self):
+        """Returns all the artifact types that are used, in alphabetical order."""
+        t = {}
+        for a in self._artifacts:
+            t[a.artifact_type] = 1
+        types = t.keys()
+        types.sort()
+        return types
+
+class TracDatabase(object):
+    def __init__(self, path):
+        self.env = trac.env.Environment(path)
+        self._db = self.env.get_db_cnx()
+        self._db.autocommit = False
+    
+    def db(self):
+        return self._db
+    
+    def hasTickets(self):
+        c = self.db().cursor()
+        c.execute('''SELECT count(*) FROM Ticket''')
+        return int(c.fetchall()[0][0]) > 0
+    
+    def setTypeList(self, s):
+        """Remove all types, set them to `s`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM enum WHERE kind='ticket_type'""")
+        for i, value in enumerate(s):
+            c.execute("""INSERT INTO enum (kind, name, value) VALUES (%s, %s, %s)""",
+                      "ticket_type",
+                      value,
+                      i)
+        self.db().commit()
+    
+    def setPriorityList(self, s):
+        """Remove all priorities, set them to `s`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM enum WHERE kind='priority'""")
+        for i, value in enumerate(s):
+            c.execute("""INSERT INTO enum (kind, name, value) VALUES (%s, %s, %s)""",
+                      "priority",
+                      value,
+                      i)
+        self.db().commit()
+
+    
+    def setComponentList(self, l):
+        """Remove all components, set them to `l`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM component""")
+        for value in l:
+            c.execute("""INSERT INTO component (name) VALUES (%s)""",
+                      value)
+        self.db().commit()
+    
+    def setVersionList(self, v):
+        """Remove all versions, set them to `v`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM version""")
+        for value in v:
+            c.execute("""INSERT INTO version (name) VALUES (%s)""",
+                      value)
+        self.db().commit()
+        
+    def setMilestoneList(self, m):
+        """Remove all milestones, set them to `m`"""
+        if self.hasTickets():
+            raise Exception("Will not modify database with existing tickets!")
+        
+        c = self.db().cursor()
+        c.execute("""DELETE FROM milestone""")
+        for value in m:
+            c.execute("""INSERT INTO milestone (name) VALUES (%s)""",
+                      value)
+        self.db().commit()
+    
+    def addTicket(self, type, time, changetime, component,
+                  priority, owner, reporter, cc,
+                  version, milestone, status, resolution,
+                  summary, description, keywords):
+        c = self.db().cursor()
+        if status.lower() == 'open':
+            if owner != '':
+                status = 'assigned'
+            else:
+                status = 'new'
+
+        c.execute("""INSERT INTO ticket (type, time, changetime, component,
+                                         priority, owner, reporter, cc,
+                                         version, milestone, status, resolution,
+                                         summary, description, keywords)
+                                 VALUES (%s, %s, %s,
+                                         %s, %s, %s, %s, %s,
+                                         %s, %s, %s, %s,
+                                         %s, %s, %s)""",
+                  type, time, changetime, component,
+                  priority, owner, reporter, cc,
+                  version, milestone, status.lower(), resolution,
+                  summary, '{{{\n%s\n}}}' % (description, ), keywords)
+        self.db().commit()
+        return self.db().db.sqlite_last_insert_rowid()
+    
+    def addTicketComment(self, ticket, time, author, value):
+        c = self.db().cursor()
+        c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue)
+                                 VALUES        (%s, %s, %s, %s, %s, %s)""",
+                  ticket, time.strftime('%s'), author, 'comment', '', '{{{\n%s\n}}}' % (value, ))
+        self.db().commit()
+
+    def addTicketChange(self, ticket, time, author, field, oldvalue, newvalue):
+        c = self.db().cursor()
+        c.execute("""INSERT INTO ticket_change (ticket, time, author, field, oldvalue, newvalue)
+                                 VALUES        (%s, %s, %s, %s, %s, %s)""",
+                  ticket, time.strftime('%s'), author, field, oldvalue, newvalue)
+        self.db().commit()
+
+
+def main():
+    import optparse
+    p = optparse.OptionParser('usage: %prog xml_export.xml /path/to/trac/environment')
+    opt, args = p.parse_args()
+    if len(args) != 2:
+        p.error("Incorrect number of arguments")
+    
+    try:
+        importData(open(args[0]), args[1])
+    except Exception, e:
+        print 'Error:', e
+
+def importData(f, env):
+    project = ExportedProjectData(f)
+    
+    db = TracDatabase(env)
+    db.setTypeList(project.artifactTypes())
+    db.setComponentList(project.categories())
+    db.setPriorityList(range(1, 11))
+    db.setVersionList(project.groups())
+    db.setMilestoneList([])
+    
+    for a in project.artifacts():
+        i = db.addTicket(type=a.artifact_type,
+                         time=a.open_date,
+                         changetime='',
+                         component=a.category,
+                         priority=a.priority,
+                         owner=a.assigned_to,
+                         reporter=a.submitted_by,
+                         cc='',
+                         version=a.artifact_group_id,
+                         milestone='',
+                         status=a.status,
+                         resolution=a.resolution,
+                         summary=a.summary,
+                         description=a.details,
+                         keywords='')
+        print 'Imported %s as #%d' % (a.artifact_id, i)
+        for msg in a.messages():
+            db.addTicketComment(ticket=i,
+                                time=msg.adddate,
+                                author=msg.user_name,
+                                value=msg.body)
+        if a.messages():
+            print '    imported %d messages for #%d' % (len(a.messages()), i)
+        
+        values = a.__dict__.copy()
+        field_map = {'summary': 'summary'}
+        for h in a.history():
+            if h.field_name == 'close_date' and values.get(h.field_name, '') == '':
+                f = 'status'
+                oldvalue = 'assigned'
+                newvalue = 'closed'
+            else:
+                f = field_map.get(h.field_name, None)
+                oldvalue = h.old_value
+                newvalue = values.get(h.field_name, '')
+                
+            if f:
+                db.addTicketChange(ticket=i,
+                                   time=h.entrydate,
+                                   author=h.mod_by,
+                                   field=f,
+                                   oldvalue=oldvalue,
+                                   newvalue=newvalue)
+            values[h.field_name] = h.old_value
+
+if __name__ == '__main__':
+    main()
new file mode 100755
--- /dev/null
+++ b/examples/trac/contrib/trac-post-commit-hook
@@ -0,0 +1,196 @@
+#!/usr/bin/env python
+
+# trac-post-commit-hook
+# ----------------------------------------------------------------------------
+# Copyright (c) 2004 Stephen Hansen 
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to
+# deal in the Software without restriction, including without limitation the
+# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+# sell copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+#   The above copyright notice and this permission notice shall be included in
+#   all copies or substantial portions of the Software. 
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+# IN THE SOFTWARE.
+# ----------------------------------------------------------------------------
+
+# This Subversion post-commit hook script is meant to interface to the
+# Trac (http://www.edgewall.com/products/trac/) issue tracking/wiki/etc 
+# system.
+# 
+# It should be called from the 'post-commit' script in Subversion, such as
+# via:
+#
+# REPOS="$1"
+# REV="$2"
+# LOG=`/usr/bin/svnlook log -r $REV $REPOS`
+# AUTHOR=`/usr/bin/svnlook author -r $REV $REPOS`
+# TRAC_ENV='/somewhere/trac/project/'
+# TRAC_URL='http://trac.mysite.com/project/'
+#
+# /usr/bin/python /usr/local/src/trac/contrib/trac-post-commit-hook \
+#  -p "$TRAC_ENV"  \
+#  -r "$REV"       \
+#  -u "$AUTHOR"    \
+#  -m "$LOG"       \
+#  -s "$TRAC_URL"
+#
+# It searches commit messages for text in the form of:
+#   command #1
+#   command #1, #2
+#   command #1 & #2 
+#   command #1 and #2
+#
+# You can have more then one command in a message. The following commands
+# are supported. There is more then one spelling for each command, to make
+# this as user-friendly as possible.
+#
+#   closes, fixes
+#     The specified issue numbers are closed with the contents of this
+#     commit message being added to it. 
+#   references, refs, addresses, re 
+#     The specified issue numbers are left in their current status, but 
+#     the contents of this commit message are added to their notes. 
+#
+# A fairly complicated example of what you can do is with a commit message
+# of:
+#
+#    Changed blah and foo to do this or that. Fixes #10 and #12, and refs #12.
+#
+# This will close #10 and #12, and add a note to #12.
+
+import re
+import os
+import sys
+import time 
+
+from trac.env import open_environment
+from trac.ticket.notification import TicketNotifyEmail
+from trac.ticket import Ticket
+from trac.ticket.web_ui import TicketModule
+# TODO: move grouped_changelog_entries to model.py
+from trac.web.href import Href
+
+try:
+    from optparse import OptionParser
+except ImportError:
+    try:
+        from optik import OptionParser
+    except ImportError:
+        raise ImportError, 'Requires Python 2.3 or the Optik option parsing library.'
+
+parser = OptionParser()
+parser.add_option('-e', '--require-envelope', dest='env', default='',
+                  help='Require commands to be enclosed in an envelope. If -e[], '
+                       'then commands must be in the form of [closes #4]. Must '
+                       'be two characters.')
+parser.add_option('-p', '--project', dest='project',
+                  help='Path to the Trac project.')
+parser.add_option('-r', '--revision', dest='rev',
+                  help='Repository revision number.')
+parser.add_option('-u', '--user', dest='user',
+                  help='The user who is responsible for this action')
+parser.add_option('-m', '--msg', dest='msg',
+                  help='The log message to search.')
+parser.add_option('-s', '--siteurl', dest='url',
+                  help='The base URL to the project\'s trac website (to which '
+                       '/ticket/## is appended).  If this is not specified, '
+                       'the project URL from trac.ini will be used.')
+
+(options, args) = parser.parse_args(sys.argv[1:])
+
+if options.env:
+    leftEnv = '\\' + options.env[0]
+    rghtEnv = '\\' + options.env[1]
+else:
+    leftEnv = ''
+    rghtEnv = ''
+
+commandPattern = re.compile(leftEnv + r'(?P<action>[A-Za-z]*).?(?P<ticket>#[0-9]+(?:(?:[, &]*|[ ]?and[ ]?)#[0-9]+)*)' + rghtEnv)
+ticketPattern = re.compile(r'#([0-9]*)')
+
+class CommitHook:
+    _supported_cmds = {'close':      '_cmdClose',
+                       'closed':     '_cmdClose',
+                       'closes':     '_cmdClose',
+                       'fix':        '_cmdClose',
+                       'fixed':      '_cmdClose',
+                       'fixes':      '_cmdClose',
+                       'addresses':  '_cmdRefs',
+                       're':         '_cmdRefs',
+                       'references': '_cmdRefs',
+                       'refs':       '_cmdRefs',
+                       'see':        '_cmdRefs'}
+
+    def __init__(self, project=options.project, author=options.user,
+                 rev=options.rev, msg=options.msg, url=options.url):
+        self.author = author
+        self.rev = rev
+        self.msg = "(In [%s]) %s" % (rev, msg)
+        self.now = int(time.time()) 
+        self.env = open_environment(project)
+        if url is None:
+            url = self.env.config.get('project', 'url')
+        class Request(object):
+            def __init__(self):
+                self.href = self.abs_href = Href(url)
+
+        cmdGroups = commandPattern.findall(msg)
+
+        tickets = {}
+        for cmd, tkts in cmdGroups:
+            funcname = CommitHook._supported_cmds.get(cmd.lower(), '')
+            if funcname:
+                for tkt_id in ticketPattern.findall(tkts):
+                    func = getattr(self, funcname)
+                    tickets.setdefault(tkt_id, []).append(func)
+
+        for tkt_id, cmds in tickets.iteritems():
+            try:
+                db = self.env.get_db_cnx()
+                
+                ticket = Ticket(self.env, int(tkt_id), db)
+                for cmd in cmds:
+                    cmd(ticket)
+
+                # determine sequence number... 
+                cnum = 0
+                tm = TicketModule(self.env)
+                for change in tm.grouped_changelog_entries(ticket, db):
+                    if change['permanent']:
+                        cnum += 1
+                
+                ticket.save_changes(self.author, self.msg, self.now, db, cnum+1)
+                db.commit()
+                
+                tn = TicketNotifyEmail(self.env)
+                tn.notify(ticket, Request(), newticket=0, modtime=self.now)
+            except Exception, e:
+                # import traceback
+                # traceback.print_exc(file=sys.stderr)
+                print>>sys.stderr, 'Unexpected error while processing ticket ' \
+                                   'ID %s: %s' % (tkt_id, e)
+            
+
+    def _cmdClose(self, ticket):
+        ticket['status'] = 'closed'
+        ticket['resolution'] = 'fixed'
+
+    def _cmdRefs(self, ticket):
+        pass
+
+
+if __name__ == "__main__":
+    if len(sys.argv) < 5:
+        print "For usage: %s --help" % (sys.argv[0])
+    else:
+        CommitHook()
new file mode 100644
--- /dev/null
+++ b/examples/trac/contrib/trac-pre-commit-hook
@@ -0,0 +1,63 @@
+#!/usr/bin/env python
+# -*- coding: iso8859-1 -*-
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#
+# This script will enforce the following policy:
+#
+#  "A checkin must reference an open ticket."
+#
+# This script should be invoked from the subversion pre-commit hook like this:
+#
+#  REPOS="$1"
+#  TXN="$2"
+#  TRAC_ENV="/somewhere/trac/project/"
+#  LOG=`/usr/bin/svnlook log -t "$TXN" "$REPOS"`
+#  /usr/bin/python /some/path/trac-pre-commit-hook "$TRAC_ENV" "$LOG" || exit 1
+#
+import os
+import re
+import sys
+
+from trac.env import open_environment
+
+def main():
+    if len(sys.argv) != 3:
+        print >> sys.stderr, 'Usage: %s <trac_project> <log_message>' % sys.argv[0]
+        sys.exit(1)
+
+    env_path = sys.argv[1]
+    log = sys.argv[2]
+
+    tickets = []
+    for tmp in re.findall('(?:closes|fixes|addresses|references|refs|re)'
+                          '.?(#[0-9]+(?:(?:[, &]+| *and *)#[0-9]+)*)', log):
+        tickets += re.findall('#([0-9]+)', tmp)
+    
+    # At least one ticket has to be mentioned in the log message
+    if tickets == []:
+        print >> sys.stderr, 'At least one open ticket must be mentioned ' \
+              'in the log message.'
+        sys.exit(1)
+
+    env = open_environment(env_path)
+    db = env.get_db_cnx()
+
+    cursor = db.cursor()
+    cursor.execute("SELECT COUNT(id) FROM ticket WHERE "
+                   "status <> 'closed' AND id IN (%s)" % ','.join(tickets))
+    row = cursor.fetchone()
+    # At least one of the tickets mentioned in the log messages has to
+    # be open
+    if not row or row[0] < 1:
+        print >> sys.stderr, 'At least one open ticket must be mentioned ' \
+              'in the log message.'
+        sys.exit(1)
+    else:
+        sys.exit(0)
+
+if __name__ == '__main__':
+    main()
+
+
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/doc/README
@@ -0,0 +1,2 @@
+Sorry, for now the only documentation we have is what's on the project website:
+http://projects.edgewall.com/trac/
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d946c78dd213258ecb13a38e5fffcdc2b9f02222
GIT binary patch
literal 453
zc%17D@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJbFq_W2nPqp?T7vkfZXr^pAgp{
z@85rY^5oOqyC1G!e{=ra>ysy69Y6kT@7~8-w?5jm>E4>vcb6``IeYfCNt3Sh_FQOb
zIaghMvY_B-X2y}k#Dh^$`vL>^czf?~a@t~HvB}72orcC5Ma5MzG7rsH-2z%6TN30K
z%<vx%czjgsJWwHLfk$L91H)Da5N2F5hu;t=C|2ScQQ};bnpl#VpQjL#nVZUBXrOOo
zrf*=<wNXDBs6r8>A~-*-q&%@GmBBYLxhOw3H6=4q!7Z~WwLHHlyI8?k&)|Kg;Rm4N
zUQZXtkcv66{<p=N40yU@QurjMD@vdJ_upDq`0ho!CtsF$&Snbd{jvIS;4eMr;Eo8*
z9~WvlUq^pQY1tWK=o%4Zu-81?Y1Zr|4g1cwRVgy3ZM53xwMP5S#?=DCFa13q_a;>z
zUvce?=cPn}!Wpq~tHjH5ROUZBwXx7GL`hb4_jL(FAJx4DE9>iI?=G)qJh6mBT#a`{
QCeUFFp00i_>zopr0P^O@tpET3
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c63d4127e0ae88af44c864765f57aad54dffb3be
GIT binary patch
literal 683
zc%17D@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dy%*9TgAsieWw;%dH0CIBzd_r9R
zeE<Ia-Meqko_)G|_x-hNZ_k~3b>{TzlP6ysJ^F0#-p5<EJlwqb{<?K{mn^wGcka!(
zbFNRFdZoAbLQBips;Uze<)=$ZP8Jm%&&@fLmUbj5>0os9-oU`!-rhT$oVHq8ZZb67
zps&A9Lt~Ai;wmYrA2aM-f%dAD1o;Is{3i^)E{Q7vs^={5h%9DcaF+mK#^R=XJAr~?
zC9V-A&PAz-C7Jno3L%-fsSJh&`bK8@1}0q_^`n6*6hSJ2^V3So6N^$Ad=rz4@^e#D
zG7}ZtGK*5n^NX^J6^!)^-e($qU|?Vj_jGX#shCq6oGaa=z%wU9a_{1)shoWO{@3T^
zO!Kn4D_&rox4n^pbCt=21xIezAG-MXm;S;Rf%+|*uSPuHQ*v&{#~E=!E1bW3TKAuS
z)4t*5?8Nlu?+ar;lx6Xqaq&OE>-vm&rAEx<UEg_bNrg<_^Q`$v-s#y9j~_07d*`Rj
zPmLK5@&gm?756ytSQovYA@JAd^97@Wlh>=gt746`eqE-1%B*>g3e(lBB`SwYSg$;q
zm-1aAM=$P!LW{S|;)5$>d$T7q&zV^-c<5AA*3O*j`%mZ3;ywS`qGqLU+{7JPZo$_*
zUL2o(kyYkNLbRl*&zGNPr)$_UJ(+7V`C`{@{f7spZ8PXOxL;(&FT0sj1SYU0FO8jU
zqpiy_>%N0R*G$7*D{}UiADz)DV%G9ufi<7<!g-!6R+nZQ)HQv1o7Sc8bw^;`2X4Uw
w$33TKZ=En*anHX$mDbbCraOf{^?$=#E~<5M_NL8B!1!SBboFyt=akR{0Cg!!`v3p{
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..513cdf700e0b07e79f78d870e169a03f07ab8541
GIT binary patch
literal 3465
zc$@)=4R-R0P)<h;3K|Lk000e1NJLTq00EEy0043X0{{R3k7}C20000mP)t-s0096J
z6cHR98zv_uF)}d1002BbJIygMU}9g1i-_KFaHy)M=%AqOxVX;D$^HNQ8$Hv$000dX
zNkl<Zc-rlpU1;1!9>BG>F}p16DlG(uK<s`zBrlEI93@AfHh1M{xQ9FmgGrk0T{s*9
z=cgqQE?AC(u?b|A_6S}K-X~KyE@VSOi3vnc=Ah*8lPMHRHq8;v7^1azy_PJm=KiCP
z)o3)@mAy)8yQhalmPXS2^#7dyjFv+OWaAD#$nygNvL!q}7$D2S`GWznDC{qh1cGD*
z;lhCc5d>lAAb=PEA}k#MkU9n#`*ndRd)`Kbiy>~XGM?JoAoHOlvU-E#_BhA}fZT6!
zu|C0ZdmaQEN%M$k{GH?W0SNk`RUF~Cy$DkG$Bggq??;g3p&e&ddlO`S=b6>s1Yu=X
z_2>H!r0$Ozb;y73?K_ZdfzH}7NApOsXC*R353Gddp=8g32;}i?i$nNwzZrX$0$H{}
zhIR|GI`jF6?BoFXsmb4eI+d3L61M!*B{=&e5|Blz&siHJz)BYK`56Z!o;NqNlDBv8
z_6VQJS#2krmfUFu%1XvL+j%Pj5XceF)58&SV}}=sqqi<pvUWP~`ENJ}L>E2HZqs?^
zSLE5wLfatQ-gm#%02$gTNDNNbI0xiAx}BFq>+_y%9|Zk)j|_mD4G`OJQMp(X5PZi$
z2jr2jovm*+2Tz@pqsV@LAOhfq3u48|jq^JIdFk|7^K)?BgkI4c9Et#Bi!|+JfijYL
zAv}oT=u6IEfN-BR?jrhc5&=k-v<L#^ynS{lEQnHdwTNecbT~KpkMC4G0+3Bok({`W
zw_)3hjkN+PJ0Ow^BC<qcTZ!SPOhy2b^*AX4WXRUL3!y=Hx5q^mNW!{D$$ut=qHw>~
zwnT#nw!DVJf-DfPzRUvQlmLNny^#b&Yr9C>i0OYfs0^kV$U_38$_46Hg>hmuk`#q7
z2`0!=ND!Xru<3tem!B$-du6;YKEnVhv=5T?MsgO&{g5E!o5%k5tX)$zGNh4sLQMB`
ze&!P2PCWFa@kAuwplpN&c|0{W)$o~9c$-FREEVEA=G^#4I$*N3oh&})+fN$Z+{)_j
zCIzxh*$69*XM#+~EIBHqOk)j&G9m>T^Efd_*RuXJm<CxV84gcBU6`R&zCxr_JR{Oa
zFdo^YQj$6q5Yh4w+6l-B=EGE1RB9B_oB^^ycbra9LW~dze|nUCMZ>_)5qdcKra&xf
zZ+NlbNfH{QN<}<+Je!}IaQ$z|mw81GGUg5KV|I>zEj)-XWm=i^4xrL*5|(^(AQ!xW
zWXZ9ivCtrtk?Z5MJ4>T}SIf{FGIq;NZFsz|fi;aTa|eTX+c8FoDI<b0VJr7*8OfFh
zYm;RyfgoN#^fe<DN$C-c34@B*l#ygfr!`Obv(_M<Wc&^*YfNd3fp1jP3WC>9EfZAR
z0;_%+2$Cim)tKpTN~Ojo(SeXgavo8g-}E@Joq(_g9d8U|bJ!a=?4)Gg-Cy*?%9bD}
zn`Oz?CAFx%F9ul@t}_yM+i!j8fqWhcWWb;A(jb*L0WxoPkjSpA&{1#f*1aco0P><G
zNFrL0i{4@SeWzgOH5aO)9Rg&K==>Mc`6s+jwIT)?_9nb`f4{Y3;MQ(Ih*98iGw}8K
zj6EJLh{rd%{jJj;ZHFLkV*gj`fFGnYQe(H3f01blWV*6LkRq)>eZGLmDbax}J3Xvt
ziS)`pow##j7wLqG0aA!Cjx<8uHhU;FFEn+($v_Za9>V*R#@^^aaG0=lW;@GT$~f=I
zgu{avzF^@Bk&oKiIm;_KuiE(j>NVf;nie4OAP}9-Sqv9MjM_-dTnkTCJquptFwdTb
zb6#pJ2t=bnG#A9JZXb&_X@ux^J*W*;PDPaEI;I>9;;Ve6h_Yl%i4w$Uwq9eIJ;U1w
z+aCphsI-lUE=bKc%MjsEH)wId2D_jlQtvcaB5^v<rw}<QcczhJwqL|v^p7`wyy{Gj
zZm|~+5Jk_CnM=o;fY{0>krO)PTvpF4eig6pwo{4V$YMMxl7G2fuQS64H5(Og;<YU8
zdFYv9qvoqV{WDLEEq4V{_YDD;N%PJ!*AA3hfe`iGo>^ichY6xG)BAQowrRKIorzm!
zuI8w=1Q7#<Bc}ED2OMt;WQ$fFucO*#uCLKrf)ty9um)XiPfu^sektosuNIk3ttCh`
z2qZy<dG@M^-GOXSv3r{;snq9LUz3QfK=fuHEP2Hu1F_YIEIe9Ap8fl|@7fowKmv^<
z5ri2X$a!CtYdG*)-K!N05L&ms@z}ykKZqO|h<`4$$!C0GD-h(JO*pE0jarOweww2t
zutd*JHz845f_N&XF-7wlwcg)sF;|-)_q~JL<|I-p5YJ1$-B3nnno6fv4GcsFGR6QA
zhM%*qHpEsS?kWMZl0qA`;nl9oO2iVG7s8TExGO}pDSPuMx?xXOb(sqXoF?x1$AeVu
zE0Pq&0m;%J!P>Pl`MY;Vre>Qw0$O}H(qtv<!qnu)AC62vc^$!cq)r*TA7syexXUps
zK7Nip|8XTpC5a|TodI%Te}L2(bD#SXq#=@FmiOIHAa$a@EX~?CAdX1J0yHbsce{H6
z#CEw$Z0Eagyp|Ibhp54bAOy^rgxri@0K_cCU-zZH0(;H`wnrf{3JIqxcaK8~b@McO
z1b;lj`K|FM^WzKk(nuq>GQ3%qb`~<qv%3UA^?&>`AR01<SH(g&kXkGwqn|r3Aqlcc
z5w9~_JMoTLPwL4u>Obq=l9t*9*LCj}#5Z1VZXz~_?q33D+$KSaSO|R}J`uXj<6>e_
zk+1;D=k&%DqT0BNamyU(0B&fIR|*ScGo(NqkTw6DlbRwyM0kvbN9i|{$mqh$B1Awj
zK~TT{wi>P#)dpo$`aCR%$OK`T2s4W>7IbAnK@X0~X7yR0TScp=fZ)(~iKp-<yxchq
zl2L5UnLLfOc(3_j5m_Lj^3;&=CAy3ZoI<ZYeH?!ZgZ=Rwx^n|EXE-dwKX?ajrz`lr
z_=wpUu3Lp2Ju|B?K)hp-p=-~c{lw0Ei-q+OE(wFaw86&|B*sKVdXO%134|+f8BT@)
zWClPaP^~i7oRJ$yy^M*9w|g2OhoxK<E~%uU^(fM~1dnShmC18xWE@qvPNXKx3QdOs
z4Mf@K<CNMFE+Lr<?~_P`oiZ+3_(4dFzx~uipv6L^YqbNb*7~_(PLKbWQ>1qay3wzI
zYN@H0y+-FMt|qKQ$4b~vzL1plJO)`ml(Rt&q4&BKQO+opv=LX6NV=_<s<k$qMn^=X
zD`G*7z2+6z$r)WMoLu|p#i-ixnm_h)3XQ-10Eu{qsaiySGSobF6bsc9C^>+@-as{t
z`V`4j92r2$IV}ZU5&^{M%UK40yd;+vu=&)wkWs)OuM(&~hq{5Yl7@@o=zZHp%Cku$
zh4rsAswUu$xG^*k<x)XS<0A#2j)SpDc&8Oeix>#qPs2R`L{!vt`6L(v)*^iG6gHq_
zEoH@#YTWw2XDwIMFo-y)Xdsz#3<TD%7;)%jC0#j*ext`7kU~Q1mGI7hYTae+Bs@)$
zC0R@1BeIszlc<RQPOw1&R?svDIrVX>Gz$ChL?gqQARl}Z$9iviw!wlak$7$3rVb!_
z;>CapF{OM0%(@@`1gTciYn`V!?A&aSKd7VNqsO_E7RVj07ak<Z;;B!PCWxNUI!^T!
z@J<Vfv`lwhkW4;52UcK$6t9>Pk*4#r_{k<nX|#lmJ)xyXbUZbZ*5m6r(?&1|^c>`o
zF8?;4#}Fn+sUTGfl2SeL!UCx#UiOv;FvxU%&H;(%^Ga)w)<5M$NCZFnoCWee95+=9
zp@u{%Sn-gNFp^0OvOWOLMrnXxi41^5&ROBc0+EYF1uM>)1tQ6n-ZiXRxX%s<ZWs*W
z<i7XAf~@zNH!7#GjTAuPO^_1KO>7$h&8a0Z2s)gGe{fJ)>p~3>Y$Kx}6L1uIcLSvS
z6<C0XY!FeY^j5L4OL7B5Pc#Ry0^)TP76jfqcCKp5;IKrx!J|N501v<4eiNiPs&^{L
z0QWMALBP+baTf%=i#>VT1mG1b2`I|7I0jKl4oImNPV~uGM{N+e&H<5GAm$pOFuy-%
zgQ#(M=z?s-CU^{z$@2xhOV+v;aws;_2OxjB%R#v8nE|Z=i9t7w*sM4xyCBl7_plls
znw>+^i8&pALS0m#+oeMBN~YHYDP75=5kJYxYJ8#KfJj&J_Rb%fzf@EI_Ub(p9>mC;
zMB}j>dUzs(HR)9h%mdQHuOUu9=)@W}fs3VDe<y&vz`hotpSd%YV=joweTJjaEA9-^
z_@ihXL?bE2F#u8dnY0O_@l$E^st5bASk3{__`}wY$|;fl;G~rSk^_sNQm*0~ew7Ep
zW{08E_}y>qd<s$u^*u}<dJ7<m+Rma9F~lfM(YxOQh~B}su>hkQ;sbCa#vgkNAjaeA
rHm=sf%(*pv!mr@hW&aQI%WVGxW}Zj=s%Q(000000NkvXXu0mjf1l&<;
new file mode 100644
--- /dev/null
+++ b/examples/trac/doc/trac_logo.svg
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
+"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg
+   id="svg5040"
+   sodipodi:version="0.32"
+   inkscape:version="0.39"
+   width="490.00000pt"
+   height="140.00000pt"
+   sodipodi:docbase="/home/daniel/trac/doc"
+   sodipodi:docname="trac_logo.svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://web.resource.org/cc/"
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   inkscape:export-filename="/home/daniel/website2/gfx/trac_logo.png"
+   inkscape:export-xdpi="31.444899"
+   inkscape:export-ydpi="31.444899">
+  <defs
+     id="defs5042" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="1.0000000"
+     inkscape:pageshadow="2"
+     inkscape:zoom="1.2279853"
+     inkscape:cx="246.83341"
+     inkscape:cy="39.340811"
+     inkscape:window-width="804"
+     inkscape:window-height="525"
+     inkscape:window-x="232"
+     inkscape:window-y="0" />
+  <metadata
+     id="metadata4402">
+    <rdf:RDF
+       id="RDF4403">
+      <cc:Work
+         rdf:about=""
+         id="Work4404">
+        <dc:format
+           id="format4405">image/svg+xml</dc:format>
+        <dc:type
+           id="type4407"
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     id="g4408"
+     transform="matrix(1.124394,-0.316118,0.316118,1.124394,-10.07967,28.50764)"
+     style="font-size:12.000000;fill:#c00000;fill-opacity:1.0000000;stroke:none;stroke-width:1.2500000;stroke-opacity:1.0000000;"
+     sodipodi:fill-cmyk="(0.24705882 1.0000000 1.0000000 0.0000000)">
+    <path
+       d="M 1.6486500,89.754800 C 4.9170500,96.945300 11.692000,104.96700 17.575100,104.31300 C 23.458200,103.66000 29.995100,88.625000 28.687700,86.010300 C 27.380300,83.395600 22.030200,63.606300 13.475300,63.844400 C 4.9204700,64.082400 -1.6197500,82.564300 1.6486500,89.754800 z "
+       id="path4409"
+       style="fill:#c00000;fill-opacity:1.0000000;" />
+    <path
+       d="M 15.710600,47.127900 C 18.266100,56.457200 29.674900,70.538700 39.835500,69.528600 C 49.996000,68.518400 47.265100,54.196500 44.532000,44.452800 C 44.137200,43.045500 45.243700,37.900900 43.208100,32.656500 C 41.172500,27.412100 36.801600,23.146500 32.527600,23.356200 C 20.408300,23.950700 12.442200,39.937400 15.710600,47.127900 z "
+       id="path4410"
+       style="fill:#c00000;fill-opacity:1.0000000;" />
+    <path
+       d="M 57.772000,55.326400 C 61.040400,62.516800 60.329800,68.756400 67.995200,70.597900 C 75.660600,72.439400 86.537900,56.076500 88.375600,48.730200 C 90.213200,41.383900 87.870300,22.968500 74.589100,22.999800 C 61.307800,23.031000 56.642400,37.798800 57.772000,55.326400 z "
+       id="path4411"
+       style="fill:#c00000;fill-opacity:1.0000000;" />
+    <path
+       d="M 81.297900,86.694200 C 80.645300,94.241100 84.925100,100.83700 94.016300,100.89600 C 103.10700,100.95600 111.42700,79.861100 110.11900,77.246400 C 108.81200,74.631700 108.09600,62.684400 99.540700,62.922400 C 90.985900,63.160500 80.881100,76.652100 81.297900,86.694200 z "
+       id="path4412"
+       style="fill:#c00000;fill-opacity:1.0000000;" />
+    <path
+       d="M 23.909000,124.83500 C 34.089300,132.60000 41.431500,121.82000 55.063700,121.87800 C 68.696000,121.93600 77.924600,130.69300 84.748500,120.50100 C 93.494500,107.43800 81.348900,106.35200 73.886200,97.368600 C 66.423500,88.384900 70.849800,81.744800 55.582000,81.167400 C 39.120500,80.544900 42.595800,98.170200 35.022800,102.13200 C 24.026800,107.88500 17.628900,120.04400 23.909000,124.83500 z "
+       id="path4413"
+       style="fill:#c00000;fill-opacity:1.0000000;" />
+    <path
+       d="M 0.018737800,54.969900 C 0.24325600,57.967700 4.2241800,61.936300 6.9705200,61.544000 C 9.7168600,61.151700 10.399900,58.323400 10.225300,54.576000 C 10.050700,50.828700 5.8332200,44.980500 3.7262000,45.218600 C 1.6191700,45.456600 -0.20578000,51.972100 0.018737800,54.969900 z "
+       id="path4414"
+       style="fill:#c00000;fill-opacity:1.0000000;" />
+    <path
+       d="M 23.956000,13.757400 C 24.180500,16.755200 24.161500,21.973800 28.407800,21.581500 C 32.654100,21.189100 34.587200,18.860900 34.412600,15.113500 C 34.237900,11.366100 31.020500,5.0180100 28.913500,5.2560400 C 26.806500,5.4940800 23.731500,10.759600 23.956000,13.757400 z "
+       id="path4415"
+       style="fill:#c00000;fill-opacity:1.0000000;" />
+    <path
+       d="M 71.206000,10.757400 C 71.430500,13.755200 75.161500,19.723800 77.907800,19.331500 C 80.654100,18.939100 83.337200,14.110900 83.162600,10.363500 C 82.987900,6.6161500 79.270500,-0.23199500 77.163500,0.0060424800 C 75.056500,0.24408000 70.981500,7.7595800 71.206000,10.757400 z "
+       id="path4416"
+       style="fill:#c00000;fill-opacity:1.0000000;" />
+    <path
+       d="M 98.456000,55.007400 C 98.680500,58.005200 99.661500,61.223800 102.40800,60.831500 C 105.15400,60.439100 108.58700,58.860900 108.41300,55.113500 C 108.23800,51.366100 107.77100,42.768000 105.66300,43.006000 C 103.55600,43.244100 98.231500,52.009600 98.456000,55.007400 z "
+       id="path4417"
+       style="fill:#c00000;fill-opacity:1.0000000;" />
+  </g>
+  <path
+     style="font-size:170.00000;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:semi-expanded;fill:#000000;fill-opacity:1.0000000;stroke:none;stroke-width:1.0000000pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0000000;font-family:Myriad Pro SemiExt;text-anchor:start;writing-mode:lr;"
+     d="M 432.73979,100.28021 C 428.31980,102.15021 422.87979,103.34021 415.73979,103.34021 C 402.47981,103.34021 390.91979,95.690197 390.91979,80.220213 C 390.74979,66.960226 400.43981,57.440213 415.05979,57.440213 C 422.87979,57.440213 427.97980,58.800214 431.54979,60.500213 L 435.96979,40.610213 C 429.50980,38.230215 421.00979,37.040213 413.69979,37.040213 C 380.20983,37.040213 363.03979,57.610237 363.03979,81.410213 C 363.03979,107.25019 381.22982,124.25021 410.12979,124.25021 C 421.00978,124.25021 430.86980,122.38021 436.30979,119.83021 L 432.73979,100.28021 M 360.20355,73.250213 C 360.20355,52.850233 349.83352,37.040213 321.27355,37.040213 C 304.95357,37.040213 293.05354,41.290216 286.93355,44.860213 L 291.86355,61.860213 C 297.64354,58.460216 307.67356,55.230213 316.68355,55.230213 C 331.30354,55.230213 333.17355,62.030216 333.17355,65.600213 L 333.17355,66.450213 C 300.19358,66.450213 280.81355,77.160234 280.81355,98.750213 C 280.81355,111.50020 291.01357,124.25021 309.20355,124.25021 C 319.91354,124.25021 329.09356,120.68021 334.87355,113.88021 L 335.55355,113.88021 L 337.08355,122.21021 L 361.56355,122.21021 C 360.71355,117.79022 360.20355,109.97020 360.20355,101.64021 L 360.20355,73.250213 M 334.02355,90.590213 C 334.02355,92.120211 333.85355,93.820214 333.51355,95.180213 C 331.81355,100.62021 325.86354,105.55021 318.72355,105.55021 C 311.92356,105.55021 307.67355,102.15021 307.67355,96.030213 C 307.67355,86.680222 318.21357,83.110213 334.02355,83.280213 L 334.02355,90.590213 M 228.42981,122.21021 L 255.45981,122.21021 L 255.45981,80.220213 C 255.45981,78.520214 255.45981,76.820211 255.79981,75.290213 C 257.32981,67.300221 263.78982,61.690213 274.83981,61.690213 C 278.06980,61.690213 280.44981,62.030213 282.65981,62.540213 L 282.65981,37.550213 C 280.44981,37.040213 279.25981,37.040213 276.87981,37.040213 C 268.20982,37.040213 256.81980,42.310226 252.39981,55.570213 L 251.71981,55.570213 L 250.86981,39.080213 L 227.74981,39.080213 C 228.25981,46.050206 228.42981,53.700226 228.42981,67.300213 L 228.42981,122.21021 M 177.48841,23.270213 L 177.48841,39.080213 L 166.09841,39.080213 L 166.09841,59.310213 L 177.48841,59.310213 L 177.48841,91.440213 C 177.48841,102.83020 179.86841,111.16022 184.62841,116.43021 C 188.87840,120.85021 196.35842,124.25021 205.36841,124.25021 C 213.01840,124.25021 219.64841,123.06021 223.21841,121.70021 L 222.87841,101.30021 C 220.32841,101.98021 218.79840,102.15021 214.54841,102.15021 C 205.87842,102.15021 203.83841,96.880203 203.83841,86.680213 L 203.83841,59.310213 L 223.21841,59.310213 L 223.21841,39.080213 L 203.83841,39.080213 L 203.83841,16.300213 L 177.48841,23.270213"
+     id="text4418"
+     sodipodi:nodetypes="cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" />
+  <path
+     style="font-size:36.000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:condensed;fill:#000000;fill-opacity:1.0000000;stroke:none;stroke-width:1.0000000pt;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1.0000000;font-family:Myriad Pro Cond;text-anchor:start;writing-mode:lr;"
+     d="M 600.81909,142.37206 L 600.81909,145.32406 L 598.69509,145.32406 L 598.69509,147.41206 L 600.81909,147.41206 L 600.81909,157.52806 C 600.81909,159.94006 601.14309,161.12806 601.86309,161.92006 C 602.51109,162.67606 603.48309,162.96406 604.59909,162.96406 C 605.49909,162.96406 606.14709,162.85606 606.65109,162.67606 L 606.50709,160.55206 C 606.21909,160.62406 605.85909,160.66006 605.39109,160.66006 C 604.38309,160.66006 603.59109,160.08406 603.59109,157.78006 L 603.59109,147.41206 L 606.93909,147.41206 L 606.93909,145.32406 L 603.59109,145.32406 L 603.59109,141.25606 L 600.81909,142.37206 M 585.65240,162.74806 L 588.42440,162.74806 L 588.42440,151.76806 C 588.42440,151.15606 588.46040,150.61606 588.56840,150.22006 C 588.89240,148.34806 590.00841,147.37606 591.16040,147.37606 C 593.17640,147.37606 593.57240,149.42806 593.57240,151.51606 L 593.57240,162.74806 L 596.34440,162.74806 L 596.34440,151.04806 C 596.34440,146.80007 594.54440,145.03606 592.13240,145.03606 C 590.29641,145.03606 588.89240,146.00806 588.13640,147.41206 L 588.06440,147.41206 L 587.92040,145.32406 L 585.50840,145.32406 C 585.54440,146.72806 585.65240,147.77206 585.65240,149.82406 L 585.65240,162.74806 M 582.58228,154.28806 C 582.61828,153.85606 582.65428,153.38806 582.65428,152.70406 C 582.65428,148.78007 581.21428,145.03606 577.68628,145.03606 C 574.19428,145.03606 571.81828,148.56407 571.81828,154.21606 C 571.81828,159.58006 573.97828,162.96406 578.11828,162.96406 C 579.52228,162.96406 580.99828,162.67606 581.97028,162.13606 L 581.50228,160.12006 C 580.81828,160.48006 579.84628,160.76806 578.62228,160.76806 C 576.64228,160.76806 574.55428,159.65206 574.51828,154.28806 L 582.58228,154.28806 M 574.55428,152.23606 C 574.66228,149.86006 575.59828,147.19606 577.39828,147.19606 C 578.73028,147.19606 580.02628,148.74407 579.99028,152.23606 L 574.55428,152.23606 M 550.28522,162.74806 L 553.05722,162.74806 L 553.05722,151.66006 C 553.05722,151.12006 553.09322,150.61606 553.16522,150.18406 C 553.48922,148.31206 554.60522,147.34006 555.75722,147.34006 C 557.80921,147.34006 558.13322,149.39206 558.13322,151.30006 L 558.13322,162.74806 L 560.90522,162.74806 L 560.90522,151.51606 C 560.90522,150.94006 560.90522,150.40006 561.01322,149.96806 C 561.40922,148.31206 562.41722,147.34006 563.56922,147.34006 C 565.65721,147.34006 565.98122,149.53606 565.98122,151.80406 L 565.98122,162.74806 L 568.75322,162.74806 L 568.75322,151.19206 C 568.75322,146.72807 566.91721,145.03606 564.54122,145.03606 C 563.64122,145.03606 562.84922,145.25206 562.12922,145.68406 C 561.51722,146.11606 560.90522,146.80006 560.43722,147.66406 L 560.36522,147.66406 C 559.71722,145.79206 558.31322,145.03606 556.69322,145.03606 C 554.85722,145.03606 553.56122,146.04406 552.80522,147.44806 L 552.73322,147.44806 L 552.58922,145.32406 L 550.14122,145.32406 C 550.17722,146.72806 550.28522,147.77206 550.28522,149.82406 L 550.28522,162.74806 M 547.21509,154.28806 C 547.25109,153.85606 547.28709,153.38806 547.28709,152.70406 C 547.28709,148.78007 545.84709,145.03606 542.31909,145.03606 C 538.82710,145.03606 536.45109,148.56407 536.45109,154.21606 C 536.45109,159.58006 538.61110,162.96406 542.75109,162.96406 C 544.15509,162.96406 545.63109,162.67606 546.60309,162.13606 L 546.13509,160.12006 C 545.45109,160.48006 544.47909,160.76806 543.25509,160.76806 C 541.27509,160.76806 539.18709,159.65206 539.15109,154.28806 L 547.21509,154.28806 M 539.18709,152.23606 C 539.29509,149.86006 540.23109,147.19606 542.03109,147.19606 C 543.36309,147.19606 544.65909,148.74407 544.62309,152.23606 L 539.18709,152.23606 M 533.37534,150.76006 C 533.37534,148.06006 533.48334,146.51206 533.51934,145.32406 L 531.03534,145.32406 L 530.89134,147.26806 L 530.81934,147.26806 C 530.27934,146.26006 529.41534,145.03606 527.57934,145.03606 C 524.98734,145.03606 522.10734,147.66407 522.10734,154.10806 C 522.10734,159.22006 524.12335,162.71206 527.21934,162.71206 C 528.65934,162.71206 529.95534,161.95606 530.53134,160.58806 L 530.60334,160.58806 L 530.60334,162.35206 C 530.60334,166.31206 529.23534,167.68006 527.03934,167.68006 C 525.67134,167.68006 524.51934,167.28406 523.83534,166.92406 L 523.18734,169.08406 C 524.08734,169.58806 525.52734,169.94806 527.00334,169.94806 C 528.94734,169.94806 530.56734,169.26406 531.64734,168.11206 C 532.79934,166.81606 533.37534,165.08806 533.37534,160.51606 L 533.37534,150.76006 M 530.60334,156.37606 C 530.60334,156.88006 530.60334,157.42006 530.42334,157.96006 C 529.88334,160.04806 528.80334,160.44406 527.93934,160.44406 C 526.28334,160.44406 524.87934,158.42806 524.87934,153.96406 C 524.87934,150.11207 525.95934,147.41206 527.93934,147.41206 C 529.52334,147.41206 530.17134,148.88806 530.45934,150.04006 C 530.56734,150.43606 530.60334,150.86806 530.60334,151.33606 L 530.60334,156.37606 M 518.96522,151.22806 C 518.96522,147.34007 517.48921,145.03606 513.99722,145.03606 C 512.26922,145.03606 510.68522,145.64806 509.92922,146.18806 L 510.54122,148.13206 C 511.36922,147.55606 512.34122,147.23206 513.42122,147.23206 C 515.61721,147.23206 516.19322,148.85206 516.19322,151.19206 L 516.19322,151.73206 C 512.26922,151.76806 508.77722,153.67607 508.77722,158.06806 C 508.77722,161.09206 510.57722,163.03606 512.91722,163.03606 C 514.39322,163.03606 515.61722,162.24406 516.37322,160.91206 L 516.48122,160.91206 L 516.66122,162.74806 L 519.14522,162.74806 C 518.96522,161.63206 518.96522,160.12006 518.96522,158.78806 L 518.96522,151.22806 M 516.26522,157.24006 C 516.26522,157.56406 516.22922,157.92406 516.15722,158.24806 C 515.83322,159.97606 514.82522,160.87606 513.60122,160.87606 C 512.62922,160.87606 511.47722,160.08406 511.47722,157.78006 C 511.47722,154.00007 514.82522,153.71206 516.26522,153.71206 L 516.26522,157.24006 M 495.23053,162.74806 L 498.00253,162.74806 L 498.00253,151.76806 C 498.00253,151.15606 498.03853,150.61606 498.14653,150.22006 C 498.47053,148.34806 499.58653,147.37606 500.73853,147.37606 C 502.75453,147.37606 503.15053,149.42806 503.15053,151.51606 L 503.15053,162.74806 L 505.92253,162.74806 L 505.92253,151.04806 C 505.92253,146.80007 504.12253,145.03606 501.71053,145.03606 C 499.87453,145.03606 498.47053,146.00806 497.71453,147.41206 L 497.64253,147.41206 L 497.49853,145.32406 L 495.08653,145.32406 C 495.12253,146.72806 495.23053,147.77206 495.23053,149.82406 L 495.23053,162.74806 M 491.33240,151.22806 C 491.33240,147.34007 489.85640,145.03606 486.36440,145.03606 C 484.63641,145.03606 483.05240,145.64806 482.29640,146.18806 L 482.90840,148.13206 C 483.73640,147.55606 484.70841,147.23206 485.78840,147.23206 C 487.98440,147.23206 488.56040,148.85206 488.56040,151.19206 L 488.56040,151.73206 C 484.63641,151.76806 481.14440,153.67607 481.14440,158.06806 C 481.14440,161.09206 482.94441,163.03606 485.28440,163.03606 C 486.76040,163.03606 487.98441,162.24406 488.74040,160.91206 L 488.84840,160.91206 L 489.02840,162.74806 L 491.51240,162.74806 C 491.33240,161.63206 491.33240,160.12006 491.33240,158.78806 L 491.33240,151.22806 M 488.63240,157.24006 C 488.63240,157.56406 488.59640,157.92406 488.52440,158.24806 C 488.20040,159.97606 487.19240,160.87606 485.96840,160.87606 C 484.99641,160.87606 483.84440,160.08406 483.84440,157.78006 C 483.84440,154.00007 487.19241,153.71206 488.63240,153.71206 L 488.63240,157.24006 M 475.97447,162.74806 L 478.56647,162.74806 L 477.16247,138.48406 L 474.06647,138.48406 L 471.33047,149.57206 C 470.53847,152.84806 469.99847,155.15206 469.53047,157.92406 L 469.42247,157.92406 C 468.91847,155.15206 468.30647,152.77606 467.47847,149.57206 L 464.67047,138.48406 L 461.64647,138.48406 L 460.20647,162.74806 L 462.69047,162.74806 L 463.15847,152.48806 C 463.30247,149.03207 463.48247,145.64806 463.44647,142.80406 L 463.55447,142.80406 C 464.05847,145.61206 464.74247,148.60007 465.57047,151.69606 L 468.37847,162.60406 L 470.17847,162.60406 L 473.13047,151.22806 C 473.88647,148.42006 474.53447,145.50406 475.07447,142.80406 L 475.21847,142.80406 C 475.11047,145.54006 475.29047,149.10407 475.47047,152.30806 L 475.97447,162.74806 M 446.34253,142.37206 L 446.34253,145.32406 L 444.21853,145.32406 L 444.21853,147.41206 L 446.34253,147.41206 L 446.34253,157.52806 C 446.34253,159.94006 446.66653,161.12806 447.38653,161.92006 C 448.03453,162.67606 449.00653,162.96406 450.12253,162.96406 C 451.02253,162.96406 451.67053,162.85606 452.17453,162.67606 L 452.03053,160.55206 C 451.74253,160.62406 451.38253,160.66006 450.91453,160.66006 C 449.90653,160.66006 449.11453,160.08406 449.11453,157.78006 L 449.11453,147.41206 L 452.46253,147.41206 L 452.46253,145.32406 L 449.11453,145.32406 L 449.11453,141.25606 L 446.34253,142.37206 M 442.93209,160.19206 C 442.32009,160.51606 441.67209,160.66006 440.95209,160.66006 C 438.39609,160.66006 436.84809,158.21206 436.84809,154.14406 C 436.84809,150.72407 438.07209,147.41206 440.88009,147.41206 C 441.78009,147.41206 442.50009,147.70006 442.86009,147.88006 L 443.40009,145.64806 C 442.89609,145.32406 441.88809,145.07206 440.95209,145.07206 C 436.59610,145.07206 434.07609,149.14007 434.07609,154.14406 C 434.07609,159.90406 436.63210,162.96406 440.44809,162.96406 C 441.67209,162.96406 442.71609,162.67606 443.32809,162.31606 L 442.93209,160.19206 M 431.76197,154.28806 C 431.79797,153.85606 431.83397,153.38806 431.83397,152.70406 C 431.83397,148.78007 430.39396,145.03606 426.86597,145.03606 C 423.37397,145.03606 420.99797,148.56407 420.99797,154.21606 C 420.99797,159.58006 423.15797,162.96406 427.29797,162.96406 C 428.70197,162.96406 430.17797,162.67606 431.14997,162.13606 L 430.68197,160.12006 C 429.99797,160.48006 429.02597,160.76806 427.80197,160.76806 C 425.82197,160.76806 423.73397,159.65206 423.69797,154.28806 L 431.76197,154.28806 M 423.73397,152.23606 C 423.84197,149.86006 424.77797,147.19606 426.57797,147.19606 C 427.90997,147.19606 429.20597,148.74407 429.16997,152.23606 L 423.73397,152.23606 M 412.41197,169.94806 C 413.49197,169.91206 415.18397,169.37206 416.22797,168.32806 C 417.45197,166.99606 417.99197,165.30406 417.99197,161.63206 L 417.99197,145.32406 L 415.21997,145.32406 L 415.21997,160.87606 C 415.21997,164.15206 415.00397,165.48406 414.39197,166.42006 C 413.81597,167.24806 412.84397,167.57206 412.05197,167.75206 L 412.41197,169.94806 M 416.62397,142.62406 C 417.59597,142.62406 418.24397,141.90406 418.24397,140.82406 C 418.24397,139.74406 417.59597,139.02406 416.62397,139.02406 C 415.68797,139.02406 414.96797,139.74406 414.96797,140.82406 C 414.96797,141.90406 415.65197,142.62406 416.58797,142.62406 L 416.62397,142.62406 M 406.15472,163.03606 C 408.81871,163.03606 411.91472,160.76806 411.91472,154.00006 C 411.91472,147.91607 409.43071,145.03606 406.29872,145.03606 C 403.31072,145.03606 400.46672,147.70007 400.46672,154.07206 C 400.46672,160.19206 402.98672,163.03606 406.11872,163.03606 L 406.15472,163.03606 M 406.22672,160.84006 C 403.74272,160.84006 403.23872,156.91606 403.23872,154.03606 C 403.23872,151.33606 403.70672,147.23206 406.19072,147.23206 C 408.63871,147.23206 409.14272,151.33606 409.14272,154.03606 C 409.14272,156.77206 408.63871,160.84006 406.26272,160.84006 L 406.22672,160.84006 M 392.25703,162.74806 L 395.02903,162.74806 L 395.02903,153.10006 C 395.02903,152.48806 395.06503,151.98406 395.13703,151.51606 C 395.42503,149.50006 396.72103,147.88006 398.48503,147.88006 C 398.70103,147.88006 398.88103,147.88006 399.06103,147.91606 L 399.06103,145.07206 C 398.88103,145.03606 398.70103,145.03606 398.48503,145.03606 C 396.86503,145.03606 395.38903,146.40406 394.81303,148.06006 L 394.74103,148.06006 L 394.59703,145.32406 L 392.11303,145.32406 C 392.14903,146.62006 392.25703,148.34806 392.25703,149.82406 L 392.25703,162.74806 M 378.16528,162.74806 L 380.93728,162.74806 L 380.93728,152.81206 C 381.33328,152.84806 381.69328,152.84806 382.16128,152.84806 C 384.32128,152.84806 386.58928,151.94806 387.88528,150.14806 C 388.82128,148.92406 389.36128,147.41206 389.36128,145.18006 C 389.36128,143.05606 388.74928,141.29206 387.59728,140.14006 C 386.33728,138.91606 384.42928,138.37606 382.19728,138.37606 C 380.57728,138.37606 379.28128,138.55606 378.16528,138.84406 L 378.16528,162.74806 M 380.93728,140.82406 C 381.29728,140.71606 381.83728,140.64406 382.48528,140.64406 C 385.58128,140.64406 386.58928,142.87606 386.58928,145.36006 C 386.58928,148.78006 384.75328,150.58006 382.05328,150.58006 C 381.58528,150.58006 381.26128,150.54406 380.93728,150.50806 L 380.93728,140.82406 M 369.79584,162.74806 C 368.10384,160.44406 367.16784,159.22006 366.69984,158.53606 C 368.31984,156.08806 369.11184,152.59606 369.36384,149.71606 L 366.87984,149.71606 C 366.80784,151.37206 366.48384,154.46806 365.43984,156.70006 C 363.56784,154.18006 361.73184,151.33606 360.79584,149.82406 L 360.79584,149.68006 C 363.89184,147.52006 364.97184,145.57606 364.97184,143.16406 C 364.97184,139.81607 362.91984,138.19606 360.68784,138.19606 C 357.55585,138.19606 355.71984,140.86007 355.71984,143.81206 C 355.71984,145.82806 356.43984,147.62806 357.48384,149.24806 L 357.48384,149.35606 C 354.92784,151.30006 353.77584,153.67606 353.77584,156.34006 C 353.77584,159.72406 355.79185,163.03606 359.82384,163.03606 C 361.76784,163.03606 363.74784,162.13606 365.07984,160.48006 C 365.87184,161.63206 366.26784,162.10006 366.69984,162.74806 L 369.79584,162.74806 M 360.25584,160.84006 C 357.73584,160.84006 356.43984,158.50006 356.43984,156.05206 C 356.43984,153.82006 357.51984,152.12806 358.59984,151.08406 C 360.07584,153.35206 361.76784,155.90806 363.85584,158.75206 C 362.99184,160.08406 361.73184,160.84006 360.29184,160.84006 L 360.25584,160.84006 M 360.47184,140.24806 C 362.01984,140.24806 362.52384,141.83206 362.52384,143.30806 C 362.52384,145.18006 361.51584,146.69206 359.64384,148.09606 C 358.70784,146.62006 358.27584,145.32406 358.27584,143.66806 C 358.27584,141.76006 359.03184,140.24806 360.43584,140.24806 L 360.47184,140.24806 M 342.87290,162.74806 L 345.46490,162.74806 L 344.06090,138.48406 L 340.96490,138.48406 L 338.22890,149.57206 C 337.43691,152.84806 336.89690,155.15206 336.42890,157.92406 L 336.32090,157.92406 C 335.81691,155.15206 335.20490,152.77606 334.37690,149.57206 L 331.56890,138.48406 L 328.54490,138.48406 L 327.10490,162.74806 L 329.58890,162.74806 L 330.05690,152.48806 C 330.20090,149.03207 330.38090,145.64806 330.34490,142.80406 L 330.45290,142.80406 C 330.95690,145.61206 331.64091,148.60007 332.46890,151.69606 L 335.27690,162.60406 L 337.07690,162.60406 L 340.02890,151.22806 C 340.78490,148.42006 341.43291,145.50406 341.97290,142.80406 L 342.11690,142.80406 C 342.00890,145.54006 342.18890,149.10407 342.36890,152.30806 L 342.87290,162.74806 M 324.40322,160.01206 C 323.82722,160.26406 322.92722,160.51606 321.88322,160.51606 C 318.89522,160.51606 316.15922,158.10405 316.15922,150.65206 C 316.15922,143.23607 319.21922,140.71606 321.81122,140.71606 C 323.07122,140.71606 323.64722,140.96806 324.22322,141.22006 L 324.83522,138.88006 C 324.36722,138.55606 323.28722,138.26806 321.84722,138.26806 C 317.99522,138.26806 313.24322,141.32807 313.24322,150.94006 C 313.24322,159.18405 316.66322,162.96406 321.37922,162.96406 C 322.89122,162.96406 324.11522,162.71206 324.87122,162.31606 L 324.40322,160.01206 M 300.09140,161.88406 C 300.77540,162.42406 302.53941,162.96406 304.19540,162.96406 C 308.04740,162.96406 310.99940,160.33606 310.99940,156.16006 C 310.99940,153.46006 309.70340,151.22806 306.78740,149.17606 C 304.48341,147.55606 303.22340,146.58406 303.22340,144.17206 C 303.22340,142.51606 304.33941,140.71606 306.71540,140.71606 C 307.97540,140.71606 308.98341,141.11206 309.59540,141.54406 L 310.27940,139.16806 C 309.70341,138.77206 308.37140,138.26806 306.85940,138.26806 C 303.04341,138.26806 300.45140,140.96807 300.45140,144.64006 C 300.45140,147.77206 303.11541,150.04006 305.02340,151.37206 C 306.93140,152.81206 308.22740,154.18006 308.22740,156.41206 C 308.22740,158.86006 306.71540,160.51606 304.19540,160.51606 C 302.97141,160.51606 301.53140,160.04806 300.70340,159.54406 L 300.09140,161.88406 M 288.79865,137.83606 L 288.79865,147.08806 L 288.72665,147.08806 C 288.04266,145.61206 286.81865,145.03606 285.55865,145.03606 C 282.85866,145.03606 280.30265,147.91607 280.30265,154.10806 C 280.30265,159.86806 282.42666,163.03606 285.37865,163.03606 C 287.21465,163.03606 288.47466,161.81206 289.01465,160.58806 L 289.15865,160.58806 L 289.33865,162.74806 L 291.71465,162.74806 C 291.67865,161.74006 291.57065,159.76006 291.57065,158.57206 L 291.57065,137.83606 L 288.79865,137.83606 M 288.79865,156.59206 C 288.79865,157.02406 288.79865,157.42006 288.69065,157.74406 C 288.07866,160.26406 286.89065,160.73206 286.02665,160.73206 C 284.01066,160.73206 283.07465,157.78006 283.07465,154.10806 C 283.07465,150.61607 283.86666,147.41206 286.17065,147.41206 C 287.50265,147.41206 288.61865,148.99606 288.76265,150.79606 C 288.79865,151.19206 288.79865,151.58806 288.79865,151.94806 L 288.79865,156.59206 M 277.98853,154.28806 C 278.02453,153.85606 278.06053,153.38806 278.06053,152.70406 C 278.06053,148.78007 276.62053,145.03606 273.09253,145.03606 C 269.60053,145.03606 267.22453,148.56407 267.22453,154.21606 C 267.22453,159.58006 269.38453,162.96406 273.52453,162.96406 C 274.92853,162.96406 276.40453,162.67606 277.37653,162.13606 L 276.90853,160.12006 C 276.22453,160.48006 275.25253,160.76806 274.02853,160.76806 C 272.04853,160.76806 269.96053,159.65206 269.92453,154.28806 L 277.98853,154.28806 M 269.96053,152.23606 C 270.06853,149.86006 271.00453,147.19606 272.80453,147.19606 C 274.13653,147.19606 275.43253,148.74407 275.39653,152.23606 L 269.96053,152.23606 M 259.38159,142.37206 L 259.38159,145.32406 L 257.25759,145.32406 L 257.25759,147.41206 L 259.38159,147.41206 L 259.38159,157.52806 C 259.38159,159.94006 259.70559,161.12806 260.42559,161.92006 C 261.07359,162.67606 262.04559,162.96406 263.16159,162.96406 C 264.06159,162.96406 264.70959,162.85606 265.21359,162.67606 L 265.06959,160.55206 C 264.78159,160.62406 264.42159,160.66006 263.95359,160.66006 C 262.94559,160.66006 262.15359,160.08406 262.15359,157.78006 L 262.15359,147.41206 L 265.50159,147.41206 L 265.50159,145.32406 L 262.15359,145.32406 L 262.15359,141.25606 L 259.38159,142.37206 M 254.87147,151.22806 C 254.87147,147.34007 253.39546,145.03606 249.90347,145.03606 C 248.17547,145.03606 246.59147,145.64806 245.83547,146.18806 L 246.44747,148.13206 C 247.27547,147.55606 248.24747,147.23206 249.32747,147.23206 C 251.52346,147.23206 252.09947,148.85206 252.09947,151.19206 L 252.09947,151.73206 C 248.17547,151.76806 244.68347,153.67607 244.68347,158.06806 C 244.68347,161.09206 246.48347,163.03606 248.82347,163.03606 C 250.29947,163.03606 251.52347,162.24406 252.27947,160.91206 L 252.38747,160.91206 L 252.56747,162.74806 L 255.05147,162.74806 C 254.87147,161.63206 254.87147,160.12006 254.87147,158.78806 L 254.87147,151.22806 M 252.17147,157.24006 C 252.17147,157.56406 252.13547,157.92406 252.06347,158.24806 C 251.73947,159.97606 250.73147,160.87606 249.50747,160.87606 C 248.53547,160.87606 247.38347,160.08406 247.38347,157.78006 C 247.38347,154.00007 250.73147,153.71206 252.17147,153.71206 L 252.17147,157.24006 M 236.72578,162.74806 L 239.49778,162.74806 L 239.49778,153.10006 C 239.49778,152.48806 239.53378,151.98406 239.60578,151.51606 C 239.89378,149.50006 241.18978,147.88006 242.95378,147.88006 C 243.16978,147.88006 243.34978,147.88006 243.52978,147.91606 L 243.52978,145.07206 C 243.34978,145.03606 243.16978,145.03606 242.95378,145.03606 C 241.33378,145.03606 239.85778,146.40406 239.28178,148.06006 L 239.20978,148.06006 L 239.06578,145.32406 L 236.58178,145.32406 C 236.61778,146.62006 236.72578,148.34806 236.72578,149.82406 L 236.72578,162.74806 M 232.93003,150.76006 C 232.93003,148.06006 233.03803,146.51206 233.07403,145.32406 L 230.59003,145.32406 L 230.44603,147.26806 L 230.37403,147.26806 C 229.83403,146.26006 228.97003,145.03606 227.13403,145.03606 C 224.54203,145.03606 221.66203,147.66407 221.66203,154.10806 C 221.66203,159.22006 223.67803,162.71206 226.77403,162.71206 C 228.21403,162.71206 229.51003,161.95606 230.08603,160.58806 L 230.15803,160.58806 L 230.15803,162.35206 C 230.15803,166.31206 228.79003,167.68006 226.59403,167.68006 C 225.22603,167.68006 224.07403,167.28406 223.39003,166.92406 L 222.74203,169.08406 C 223.64203,169.58806 225.08203,169.94806 226.55803,169.94806 C 228.50203,169.94806 230.12203,169.26406 231.20203,168.11206 C 232.35403,166.81606 232.93003,165.08806 232.93003,160.51606 L 232.93003,150.76006 M 230.15803,156.37606 C 230.15803,156.88006 230.15803,157.42006 229.97803,157.96006 C 229.43803,160.04806 228.35803,160.44406 227.49403,160.44406 C 225.83803,160.44406 224.43403,158.42806 224.43403,153.96406 C 224.43403,150.11207 225.51403,147.41206 227.49403,147.41206 C 229.07803,147.41206 229.72603,148.88806 230.01403,150.04006 C 230.12203,150.43606 230.15803,150.86806 230.15803,151.33606 L 230.15803,156.37606 M 219.34790,154.28806 C 219.38390,153.85606 219.41990,153.38806 219.41990,152.70406 C 219.41990,148.78007 217.97990,145.03606 214.45190,145.03606 C 210.95991,145.03606 208.58390,148.56407 208.58390,154.21606 C 208.58390,159.58006 210.74391,162.96406 214.88390,162.96406 C 216.28790,162.96406 217.76391,162.67606 218.73590,162.13606 L 218.26790,160.12006 C 217.58391,160.48006 216.61190,160.76806 215.38790,160.76806 C 213.40791,160.76806 211.31990,159.65206 211.28390,154.28806 L 219.34790,154.28806 M 211.31990,152.23606 C 211.42790,149.86006 212.36391,147.19606 214.16390,147.19606 C 215.49590,147.19606 216.79190,148.74407 216.75590,152.23606 L 211.31990,152.23606 M 200.74097,142.37206 L 200.74097,145.32406 L 198.61697,145.32406 L 198.61697,147.41206 L 200.74097,147.41206 L 200.74097,157.52806 C 200.74097,159.94006 201.06497,161.12806 201.78497,161.92006 C 202.43297,162.67606 203.40497,162.96406 204.52097,162.96406 C 205.42097,162.96406 206.06897,162.85606 206.57297,162.67606 L 206.42897,160.55206 C 206.14097,160.62406 205.78097,160.66006 205.31297,160.66006 C 204.30497,160.66006 203.51297,160.08406 203.51297,157.78006 L 203.51297,147.41206 L 206.86097,147.41206 L 206.86097,145.32406 L 203.51297,145.32406 L 203.51297,141.25606 L 200.74097,142.37206 M 185.57428,162.74806 L 188.34628,162.74806 L 188.34628,151.76806 C 188.34628,151.15606 188.38228,150.61606 188.49028,150.22006 C 188.81428,148.34806 189.93028,147.37606 191.08228,147.37606 C 193.09828,147.37606 193.49428,149.42806 193.49428,151.51606 L 193.49428,162.74806 L 196.26628,162.74806 L 196.26628,151.04806 C 196.26628,146.80007 194.46628,145.03606 192.05428,145.03606 C 190.21828,145.03606 188.81428,146.00806 188.05828,147.41206 L 187.98628,147.41206 L 187.84228,145.32406 L 185.43028,145.32406 C 185.46628,146.72806 185.57428,147.77206 185.57428,149.82406 L 185.57428,162.74806 M 178.61840,138.48406 L 178.61840,162.74806 L 181.39040,162.74806 L 181.39040,138.48406 L 178.61840,138.48406"
+     id="text4421" />
+</svg>
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/README
@@ -0,0 +1,5 @@
+This directory contains files used by Trac's default clearsilver templates.
+
+Local modifications to these files might be lost during the installation of 
+a new Trac version. This can be avoided by making a copy of this entire
+directory before beginning modifications.
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..486b7315902f8bd4d7c953f4b6e65e56cb171c50
GIT binary patch
literal 222
zc%17D@N?(olHy`uVBq!ia0vp^96-#@!VDyjmkZ_sDdu7)&kzm{j@u9Y9{{=h0X`wF
zuV25udiCm=GiUbi-@kI@%BvF}%mYfXmIV0)GyDevnG0$lvp5SpB8wRqxITa|qthCb
z89+hB64!{5;QX|b^2DN42H(WwqWs*{l*~j0x6Go{^8BLgVg+M8gZG(+KwS(Po-U3d
z6?2jk5`q#E5(Elagw)RQ@bu{EG4Y(y(_`b&($n+f@p52bFt%pi>BpeA4ycF0)78&q
Iol`;+0Q%iTa{vGU
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..a4d212439158aa08cbf451a557a5f987c8f9f32a
GIT binary patch
literal 280
zc%17D@N?(olHy`uVBq!ia0vp^JRr=$3?vg*uel1OSkfJR9T^zbpD<_bdI{u9mbgZg
z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-$J2=EDU_3`lu3JQvdh=`4i&B@6r
zD=X{j>Y6ZN!tB|zckbMI^5n^j7cbtrb?e!)XYb#?2Wt9c9la7r36uo+1^-6{3=foJ
zC4q85o-U3d6}NoOy%us1<ZugIQgr(Jzx?ilg>~z``8;h%n3^2IkhduF<%B(nCd+l_
z8BIL1SLy72C-HC1@v1R1mYqp&3=!6>U2O2HTqI!GSKc-TW`-;FZ2S>7J>~#SXYh3O
Kb6Mw<&;$Szcyo3D
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..31c0356a3eaf6530f6b0a619de10c0435f063fff
GIT binary patch
literal 294
zc%17D@N?(olHy`uVBq!ia0vp^JRr=%3?!FCJ6-`&%*9TgAsieWw;%dH0CIH$d_r9R
z|NsB}`}fbEKfiwc`u_d<r%s((wQAL*Nt2qInrdrnQ&Ur8V`F`NeN9bG4Gj%dSV}hn
zHS(7P`2{olM*&ZJ=Pd(DaTa()7Bet#eE?xbr!^)sfP#u8t`Q}{`DrEPiAAXlzKO|2
z`MIennTZN+nMJAP`9;~q3MP66uhi5_fr>pmT^vIy<|HRHFrJf4m=xS3?cqB|%jzk^
z1a*xS#}6F1s8w;Y#FDvvflhm;_yqMUZ+3BMEYEqw7-E^a`nBK!uRJq-xeav-co;tP
W@~-*5I?@JcCWEJ|pUXO@geCwMCUaH*
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..43f7a844739b0875c3ea281d8c4720585d6ead4f
GIT binary patch
literal 297
zc%17D@N?(olHy`uVBq!ia0vp^JRr=%3?!FCJ6-`&%*9TgAsieWw;%dH0CFV)d_r6q
z(ilEGtvtQJ`Tmiv|NsAMPBma?VEFm9RrsysT%a0`k|4iep!k0VAl7XOQ3Z-|7I;J!
zGca%qfiUBxyLEqnf{G=s5hcO-X(i=}MX3zFiOEIzxv43ci3)C+MXBZaMcKs)CVB?1
z)YMCXip4;Ror_WvOEUBG6hbm{QyB~m^$pGS4NNZIvWx<%aP@R?45^rtT)@c8mTZt<
zpwHeU#8#}jRH-!4g5QPJc*bhBmDAM>W=vO`(r(B+m1Tum!;?cdZuB<Z@)hp!m57On
ciHwP1XxuE)yJe@aCeT0zPgg&ebxsLQ0QbXSl>h($
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/css/about.css
@@ -0,0 +1,42 @@
+/* About config */
+#content.about_config table {
+ border-collapse: collapse;
+ margin: 2em 0;
+}
+#content.about_config th {
+ background: #f7f7f0;
+ font-weight: bold;
+ text-align: left;
+ vertical-align: top;
+}
+#content.about_config th.section {
+ text-align: right;
+}
+#content.about_config th, #content.about_config td {
+ border: 1px solid #ddd;
+ padding: 3px;
+}
+#content.about_config td.name { background:#f9f9f0; }
+#content.about_config td.value { background:#f9f9f0; font-weight: bold; }
+#content.about_config td.defaultvalue { font-family: monospace; background:#f9f9f0; }
+#content.about_config td.doc { padding: 3px 1em 3px 1em; }
+
+/* About plugins */
+#content.about_plugins h2 {
+ background: #f7f7f7;
+ border-bottom: 1px solid #d7d7d7;
+ margin: 2em 0 0;
+}
+#content.about_plugins table {
+ border-collapse: collapse;
+ margin: 1em 0;
+ table-layout: fixed;
+ width: 100%;
+}
+#content.about_plugins th, #content.about_plugins td { border: 1px solid #ddd; padding: 3px }
+#content.about_plugins th { background: #f7f7f0; font-weight: bold; text-align: right; vertical-align: top; width: 12em }
+#content.about_plugins td.module { font-family: monospace; }
+#content.about_plugins td.module .path { color: #999; font-size: 90%; }
+
+#content.about_plugins td.xtnpts { margin-top: 1em; }
+#content.about_plugins td.xtnpts ul { list-style: square; margin: 0; padding: 0 0 0 2em; }
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/css/browser.css
@@ -0,0 +1,138 @@
+#prefs { margin-top: -1.6em }
+* html #prefs { width: 34em } /* Set width only for IE */
+#prefs fieldset label { display: block }
+#prefs .buttons { margin-top: -1.6em }
+#prefs .choice { margin-top: -0.6em }
+
+#legend { clear: right; }
+
+/* Browser */
+h1 { margin: 0; padding: 0 0 .5em }
+h1 :link, h1 :visited, h1 .filename { border: none; padding: 0 .2em }
+h1 :link, h1 :visited { color: #b00 }
+h1 .first:link, h1 .first:visited { color: #998 }
+h1 .sep { color: #666; padding: 0 .1em }
+
+#jumprev { float: right; font-size: 10px; margin: 0 0 0 }
+#jumprev form { margin: 0 }
+#jumprev input { font-size: 10px; margin-right: 0 }
+
+/* Styles for the directory entries table
+   (extends the styles for "table.listing") */
+#dirlist { margin-top: 0 }
+#dirlist td.rev, #dirlist td.age, #dirlist td.change {
+ color: #888;
+ white-space: nowrap;
+ vertical-align: baseline;
+}
+#dirlist td.rev {
+ font-family: monospace;
+ letter-spacing: -0.08em;
+ font-size: 90%;
+ text-align: right;
+}
+#dirlist td.size {  
+ color: #888;
+ white-space: nowrap;
+ text-align: right;
+ vertical-align: middle;
+ font-size: 70%;
+}
+#dirlist td.name { width: 100% }
+#dirlist td.name a, #dirlist td.name span {
+ background-position: 0% 50%;
+ background-repeat: no-repeat;
+ padding-left: 20px;
+}
+#dirlist td.name a.parent { background-image: url(../parent.png) }
+#dirlist td.name a.dir { background-image: url(../folder.png) }
+#dirlist td.name span.dir { background-image: url(../folderdeny.png) }
+#dirlist td.name a.file { background-image: url(../file.png) }
+#dirlist td.name span.file { background-image: url(../filedeny.png) }
+#dirlist td.name a, #dirlist td.rev a { border-bottom: none; display: block }
+#dirlist td.rev { text-align: right }
+#dirlist td.change { font-size: 85%; vertical-align: middle; white-space: nowrap }
+
+/* Style for the ''View Changes'' button */
+#anydiff {
+ margin: 0 0 1em;
+ float: left;
+}
+#anydiff form, #anydiff div, #anydiff h2 {
+ display: inline;
+}
+#anydiff input { 
+ vertical-align: baseline;
+ margin: 0 -0.5em 0 1em;
+}
+@media print {
+ #anydiff form { display:  none }
+}
+
+
+/* Log */
+tr.diff input { 
+ padding: 0 1em 0 1em;
+ margin: 0; 
+}
+
+div.buttons {
+ clear: left;
+}
+@media print { 
+  th.diff, td.diff { display: none }
+}
+/* Styles for the revision log table
+   (extends the styles for "table.listing") */
+#chglist { margin-top: 0 }
+#chglist td.change span { 
+ border: 1px solid #999;
+ display: block;
+ margin: .2em .5em 0 0;
+ width: .8em; height: .8em;
+}
+#chglist td.diff { white-space: nowrap }
+#chglist td.change .comment { display: none }
+#chglist td.old_path { font-style: italic }
+#chglist td.date { font-size: 85%; vertical-align: top; padding-top: 0.55em; white-space: nowrap }
+#chglist td.author { font-size: 85%; vertical-align: top; padding-top: 0.55em }
+#chglist td.rev, #chglist td.chgset { 
+ font-family: monospace;  
+ letter-spacing: -0.08em;
+ font-size: 90%;
+ text-align: right; 
+}
+#chglist td.rev a, #chglist td.chgset a { border-bottom: none }
+#chglist td.summary { 
+ width: 100%; 
+ font-size: 85%; 
+ vertical-align: middle; 
+ white-space: nowrap 
+}
+#chglist .verbose td.summary {
+ border: none;
+ color: #333;
+ padding: .5em 1em 1em 2em;
+ font-size: 90%; 
+ white-space: normal 
+}
+
+#chglist td.summary * { margin-top: 0; margin-bottom: 0 }
+
+#paging { margin: 1em 0 }
+
+/* Styles for the revision info in the file view (see also trac.css) */
+#info { margin: 0; }
+#info .props {
+ color: #666;
+ list-style: square;
+ margin: 0 0 .4em 1.6em;
+ padding: 0;
+}
+#info .props li { padding: 0 }
+
+/* Styles for the HTML preview */
+#preview { background: #fff; clear: both; margin: 0 }
+#preview .code-block { border-top: 1px solid #999; margin: 0 }
+#preview .image-file { overflow: hidden }
+#preview .image-file img { max-width: 100% }
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/css/changeset.css
@@ -0,0 +1,44 @@
+/* Changeset overview */
+#overview .files { padding-top: 1em }
+#overview .files ul { margin: 0; padding: 0 }
+#overview .files li { list-style-type: none }
+#overview .files li .comment { display: none }
+#overview .files li div {
+ border: 1px solid #999;
+ float: left;
+ margin: .2em .5em 0 0;
+ overflow: hidden;
+ width: .8em; height: .8em;
+}
+#overview div.add div, #overview div.cp div, #overview div.mv div {
+ border: 0;
+ margin: 0;
+ float: right;
+ width: .35em; 
+}
+
+#overview .changeset { padding: 0 0 1px }
+#overview dd.changeset p {
+ margin-bottom: 0;
+ margin-top: 0;
+}
+#overview .files { padding: 1px 0 }
+
+.diff ul.props { font-size: 90%; list-style: disc; margin: .5em 0 0; padding: 0 .5em 1em 2em }
+.diff ul.props li { margin: 0; padding: 0 }
+
+
+#title dl {
+ display: inline;
+ font-size: 110%
+}
+#title dt { 
+  font-size: 110%;
+  font-weight: bold;
+  display: inline; 
+  margin-left: 3em;
+}
+#title dd { 
+  display: inline;
+  margin-left: 0.4em;
+}
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/css/code.css
@@ -0,0 +1,161 @@
+div.code {
+ background: #f7f7f7;
+ border: 1px solid #d7d7d7;
+ margin: 1em 1.75em;
+ padding: .25em;
+ overflow: auto
+}
+
+div.code pre { margin: 0; }
+
+table.code {
+ border: 1px solid #ddd;
+ border-spacing: 0;
+ border-top: 0;
+ empty-cells: show;
+ font-size: 12px;
+ line-height: 130%;
+ padding: 0;
+ margin: 0 auto;
+ table-layout: fixed;
+ width: 100%;
+}
+table.code th {
+ border-right: 1px solid #d7d7d7;
+ border-bottom: 1px solid #998;
+ font-size: 11px;
+}
+table.code th.lineno { width: 4em }
+table.code thead th {
+ background: #eee;
+ border-top: 1px solid #d7d7d7;
+ color: #999;
+ padding: 0 .25em;
+ text-align: center;
+ white-space: nowrap;
+}
+table.code tbody th {
+ background: #eed;
+ color: #886;
+ font-weight: normal;
+ padding: 0 .5em;
+ text-align: right;
+ vertical-align: top;
+}
+table.code tbody th :link, table.code tbody th :visited {
+ border: none;
+ color: #886;
+ text-decoration: none;
+}
+table.code tbody th :link:hover, table.code tbody th :visited:hover {
+ color: #000;
+}
+table.code tbody td {
+ background: #fff;
+ font: normal 11px monospace;
+ overflow: hidden;
+ padding: 1px 2px;
+ vertical-align: top;
+}
+
+.image-file { background: #eee; padding: .3em }
+.image-file img { background: url(../imggrid.png) }
+
+/* Default */
+.code-block span {
+ font-family: monospace;
+}
+
+/* Comments */
+.code-comment, .css_comment, .c_comment, .c_commentdoc, .c_commentline,
+.c_commentlinedoc, .h_comment,.pl_commentline, .p_commentblock,
+.p_commentline, .hphp_comment, .hphp_commentblock, .hphp_commentline,
+.yaml_comment {
+ color: #998; 
+ font-style: italic;
+}
+
+/* Language keyword */
+.code-keyword, .pl_word  { color: #789; font-weight: bold }
+
+/* Type */
+.code-type, .c_word, .c_word2, .p_classname, .hphp_classname{
+ color: #468;
+ font-weight: bold;
+}
+
+/* Function */
+.code-func, .p_defname {
+ color: #900;
+ font-weight: bold;
+ border-bottom: none;
+}
+
+/* Pre-processor */
+.code-prep, .c_preprocessor, .pl_preprocessor, .yaml_identifier {
+ color: #999;
+ font-weight: bold;
+}
+
+/* Language construct */
+.code-lang, .p_word { color: #000; font-weight: bold }
+
+/* String */
+.code-string, .c_string, .c_stringeol, .css_doublestring, .css_singlestring,
+.h_singlestring, .h_doublestring, .pl_string, .pl_string_q, .pl_string_qq,
+.pl_string_qr, .pl_string_qw, .pl_string_qx, .pl_backticks, .pl_character,
+.p_string, .p_stringeol, .hphp_string, .hphp_stringeol, .hphp_triple,
+.hphp_tripledouble, .p_character, .p_triple, .p_tripledouble {
+ color: #b84;
+ font-weight: normal;
+}
+
+/* Variable name */
+.code-var { color: #f9f }
+
+/* SilverCity-specific styles */
+.css_id, .css_class, .css_pseudoclass, .css_tag { color: #900000 }
+.css_directive { color: #009000; font-weight: bold }
+.css_important { color: blue }
+.css_operator { color: #000090; font-weight: bold }
+.css_tag { font-weight: bold }
+.css_unknown_identifier, .css_unknown_pseudoclass { color: red }
+.css_value { color: navy }
+.c_commentdockeyword { color: navy; font-weight: bold }
+.c_commentdockeyworderror { color: red; font-weight: bold }
+.c_character, .c_regex, .c_uuid, .c_verbatim { color: olive }
+.c_number { color: #099 }
+.h_asp { color: #ff0 }
+.h_aspat { color: #ffdf00 }
+.h_attribute { color: teal }
+.h_attributeunknown { color: red }
+.h_cdata { color: #373 }
+.h_entity { color: purple }
+.h_number { color: #099 }
+.h_other { color: purple }
+.h_script, .h_tag, .h_tagend { color: navy }
+.h_tagunknown { color: red }
+.h_xmlend, .h_xmlstart { color: blue }
+.pl_datasection { color: olive }
+.pl_error { color: red; font-weight: bold }
+.pl_hash { color: #000 }
+.pl_here_delim, .pl_here_q, .pl_here_qq, .pl_here_qx, .pl_longquote { color: olive }
+.pl_number { color: #099 }
+.pl_pod { font-style: italic }
+.pl_regex, .pl_regsubst { color: olive }
+.p_number { color: #099 }
+.hphp_character { color: olive }
+.hphp_defname { color: #099; font-weight: bold }
+.hphp_number { color: #099 }
+.hphp_word { color: navy; font-weight: bold }
+.yaml_document { color: gray; font-style: italic }
+.yaml_keyword { color: #808 }
+.yaml_number { color: #800 }
+.yaml_reference { color: #088 }
+.v_comment { color: gray; font-style: italic }
+.v_commentline, .v_commentlinebang { color: red; font-style: italic }
+.v_number, .v_preprocessor { color: #099 }
+.v_string, .v_stringeol { color: olive }
+.v_user{ color: blue; font-weight: bold }
+.v_word, .v_word3 { color: navy; font-weight: bold }
+.v_word2 { color: green; font-weight: bold }
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/css/diff.css
@@ -0,0 +1,163 @@
+/* Diff preferences */
+#prefs fieldset { margin: 1em .5em .5em; padding: .5em 1em 0 }
+
+/* Diff/change overview */
+#overview {
+ line-height: 130%;
+ margin-top: 1em;
+ padding: .5em;
+}
+#overview dt.property {
+ font-weight: bold;
+ padding-right: .25em;
+ position: absolute;
+ left: 0;
+ text-align: right;
+ width: 7.75em;
+}
+#overview dd { margin-left: 8em }
+
+#overview .message { padding: 1em 0 1px }
+#overview dd.message p, #overview dd.message ul, #overview dd.message ol,
+#overview dd.message pre {
+ margin-bottom: 1em;
+ margin-top: 0;
+}
+
+/* Colors for change types */
+#chglist .edit, #overview .mod, .diff #legend .mod { background: #fd8 }
+#chglist .delete, #overview .rem, .diff #legend .rem { background: #f88 }
+#chglist .add, #overview .add, .diff #legend .add { background: #bfb }
+#chglist .copy, #overview .cp, .diff #legend .cp { background: #88f }
+#chglist .move, #overview .mv, .diff #legend .mv { background: #ccc }
+#chglist .unknown { background: #fff }
+
+/* Legend for diff colors */
+.diff #legend {
+ float: left;
+ font-size: 9px;
+ line-height: 1em;
+ margin: 1em 0;
+ padding: .5em;
+}
+.diff #legend h3 { display: none; }
+.diff #legend dt {
+ background: #fff;
+ border: 1px solid #999;
+ float: left;
+ margin: .1em .5em .1em 2em;
+ overflow: hidden;
+ width: .8em; height: .8em;
+}
+.diff #legend dl, .diff #legend dd {
+ display: inline;
+ float: left;
+ padding: 0;
+ margin: 0;
+ margin-right: .5em;
+}
+
+/* Styles for the list of diffs */
+.diff ul.entries { clear: both; margin: 0; padding: 0 }
+.diff li.entry {
+ background: #f7f7f7;
+ border: 1px solid #d7d7d7;
+ list-style-type: none;
+ margin: 0 0 2em;
+ padding: 2px;
+ position: relative;
+ width: 100%;
+}
+.diff h2 {
+ color: #333;
+ font-size: 14px;
+ letter-spacing: normal;
+ margin: 0 auto;
+ padding: .1em 0 .25em .5em;
+}
+
+/* Styles for the actual diff tables (side-by-side and inline) */
+.diff table {
+ border: 1px solid #ddd;
+ border-spacing: 0;
+ border-top: 0;
+ empty-cells: show;
+ font-size: 12px;
+ line-height: 130%;
+ padding: 0;
+ margin: 0 auto;
+ table-layout: fixed;
+ width: 100%;
+}
+.diff table col.lineno { width: 4em }
+.diff table th {
+ border-right: 1px solid #d7d7d7;
+ border-bottom: 1px solid #998;
+ font-size: 11px;
+}
+.diff table thead th {
+ background: #eee;
+ border-top: 1px solid #d7d7d7;
+ color: #999;
+ padding: 0 .25em;
+ text-align: center;
+ white-space: nowrap;
+}
+.diff table tbody th {
+ background: #eed;
+ color: #886;
+ font-weight: normal;
+ padding: 0 .5em;
+ text-align: right;
+ vertical-align: top;
+}
+.diff table tbody td {
+ background: #fff;
+ font: normal 11px monospace;
+ overflow: hidden;
+ padding: 1px 2px;
+ vertical-align: top;
+}
+.diff table tbody.skipped td {
+ background: #f7f7f7;
+ border: 1px solid #d7d7d7;
+}
+.diff table td del, .diff table td ins { text-decoration: none }
+.diff table td del { color: #600 }
+.diff table td ins { color: #060 }
+
+/* Styles for the inline diff */
+.diff table.inline tbody.mod td.l, .diff table.inline tbody.rem td.l {
+ background: #fdd;
+ border-color: #c00;
+ border-style: solid;
+ border-width: 0 1px 0 1px;
+}
+.diff table.inline tbody.mod td.r, .diff table.inline tbody.add td.r {
+ background: #dfd;
+ border-color: #0a0;
+ border-style: solid;
+ border-width: 0 1px 0 1px;
+}
+.diff table.inline tbody.mod tr.first td.l,
+.diff table.inline tbody.rem tr.first td.l { border-top-width: 1px }
+.diff table.inline tbody.mod tr.last td.l,
+.diff table.inline tbody.rem tr.last td.l { border-bottom-width: 1px }
+.diff table.inline tbody.mod tr.first td.r,
+.diff table.inline tbody.add tr.first td.r { border-top-width: 1px }
+.diff table.inline tbody.mod tr.last td.r,
+.diff table.inline tbody.add tr.last td.r { border-bottom-width: 1px }
+.diff table.inline tbody.mod td del { background: #e99; color: #000 }
+.diff table.inline tbody.mod td ins { background: #9e9; color: #000 }
+
+/* Styles for the side-by-side diff */
+.diff table.sidebyside colgroup.content { width: 50% }
+.diff table.sidebyside tbody.mod td.l { background: #fe9 }
+.diff table.sidebyside tbody.mod td.r { background: #fd8 }
+.diff table.sidebyside tbody.add td.l { background: #dfd }
+.diff table.sidebyside tbody.add td.r { background: #cfc }
+.diff table.sidebyside tbody.rem td.l { background: #f88 }
+.diff table.sidebyside tbody.rem td.r { background: #faa }
+.diff table.sidebyside tbody.mod del, .diff table.sidebyside tbody.mod ins {
+ background: #fc0;
+}
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/css/report.css
@@ -0,0 +1,90 @@
+@import url(code.css);
+
+h1 .numrows, h2 .numrows {
+ margin-left: 1em;
+ color: #999; 
+ font-size: 65%; 
+ font-weight: normal; 
+}
+h2 {
+ background: #f7f7f7;
+ border-bottom: 1px solid #d7d7d7;
+ margin: 2em 0 0;
+ padding: 0 .33em;
+}
+#report-descr { margin: 0 2em; font-size: 90% }
+#report-notfound { margin: 2em; font-size: 110% }
+
+#query { clear: right }
+#query fieldset, #query fieldset input, #query fieldset select { font-size: 11px }
+#query fieldset { margin-top: 1em }
+#query .option, #query .option input, #query .option select { font-size: 11px }
+#query .option { float: left; line-height: 2em; margin: .9em 2.5em 0 .5em; padding: 0 0 .1em }
+#query .buttons { float: right; margin-top: .5em }
+#query .buttons input { margin: .5em }
+#query hr { clear: both; margin: 0; visibility: hidden }
+
+#filters table { width: 100% }
+#filters tr { height: 2em }
+#filters th, #filters td { padding: 0 .2em; vertical-align: middle }
+#filters th { font-size: 11px; text-align: right; white-space: nowrap; }
+#filters td label { font-size: 11px }
+#filters td.mode { text-align: right }
+#filters td.filter { width: 100% }
+#filters td.filter label { padding-right: 1em }
+#filters td.actions { text-align: right; white-space: nowrap }
+
+/* Styles for the report list and the report results table
+   (extends the styles for "table.listing") */
+.reports td.title { width: 100% }
+.reports tbody td :link, .reports tbody td :visited,
+.tickets tbody td :link, .tickets tbody td :visited { display: block }
+.tickets { border-bottom: none }
+.tickets thead th { text-transform: capitalize; white-space: nowrap; }
+.tickets tbody td, .reports tbody td { padding: .1em .5em !important }
+.tickets tbody td a, .reports tbody td a { border-bottom: none }
+.tickets tbody td.id :link, .tickets tbody td.id :visited {
+ font-weight: bold;
+}
+.tickets tbody tr:hover { background: #eed; color: #000 }
+.tickets tr.color1-odd  { background: #fdc; border-color: #e88; color: #a22 }
+.tickets tr.color1-even { background: #fed; border-color: #e99; color: #a22 }
+.tickets tr.color2-odd  { background: #ffb; border-color: #eea; color: #880 }
+.tickets tr.color2-even { background: #ffd; border-color: #dd8; color: #880 }
+.tickets tr.color3-odd  { background: #fbfbfb; border-color: #ddd; color: #444 }
+.tickets tr.color3-even { background: #f6f6f6; border-color: #ccc; color: #333 }
+.tickets tr.color4-odd { background: #e7ffff; border-color: #cee; color: #099 }
+.tickets tr.color4-even { background: #dff; border-color: #bee; color: #099 }
+.tickets tr.color5-odd { background: #e7eeff; border-color: #cde; color: #469 }
+.tickets tr.color5-even { background: #dde7ff; border-color: #cde; color: #469 }
+.tickets tr.color6-odd  { background: #f0f0f0; border-color: #ddd; color: #888 }
+.tickets tr.color6-even { background: #f7f7f7; border-color: #ddd; color: #888 }
+.tickets tr.color6-odd a, .color6-even a { color: #b66 }
+.tickets tbody tr.fullrow td, .tickets tbody td.fullrow {
+ border: none;
+ color: #333;
+ background: transparent;
+ padding: 0 1em 2em 2em !important;
+ font-size: 85%;
+}
+.tickets tbody tr.fullrow:hover { background: transparent !important }
+.tickets .fullrow :link, .tickets .fullrow :visited { display: inline }
+.tickets .fullrow .meta { color: #999; margin-bottom: -.5em; margin-left: -1em }
+.tickets .fullrow hr { display: none }
+
+/* Query results table */
+table.tickets tbody tr.added td { font-weight: bold }
+table.tickets tbody tr.changed td { font-style: italic }
+table.tickets tbody tr.removed td { color: #999 }
+table.tickets tbody tr.prio1 { background: #fdc; border-color: #e88 }
+table.tickets tbody tr.even.prio1 { background: #fed; border-color: #e99 }
+table.tickets tbody tr.prio2 { background: #ffb; border-color: #eea }
+table.tickets tbody tr.even.prio2 { background: #ffd; border-color: #dd8 }
+table.tickets tbody tr.prio3  { background: #fbfbfb; border-color: #ddd }
+table.tickets tbody tr.even.prio3 { background: #f6f6f6; border-color: #ccc }
+table.tickets tbody tr.prio4 { background: #e7ffff; border-color: #cee }
+table.tickets tbody tr.even.prio4 { background: #dff; border-color: #bee }
+table.tickets tbody tr.prio5 { background: #e7eeff; border-color: #cde }
+table.tickets tbody tr.even.prio5 { background: #dde7ff }
+table.tickets tbody tr.prio6 { background: #f0f0f0; border-color: #ddd }
+table.tickets tbody tr.even.prio6 { background: #f7f7f7 }
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/css/roadmap.css
@@ -0,0 +1,83 @@
+/* General styles for the progress bars */
+table.progress {
+ border: 1px solid #d7d7d7;
+ border-collapse: collapse;
+ border-spacing: 0;
+ float: left;
+ margin: 0;
+ padding: 0;
+ empty-cells: show;
+}
+table.progress a, table.progress :link, table.progress :visited,
+table.progress :link:hover, table.progress :visited:hover {
+ border: none;
+ display: block;
+ width: 100%;
+ height: 1.2em;
+ padding: 0;
+ margin: 0;
+ text-decoration: none
+}
+table.progress td { background: #fff; padding: 0 }
+table.progress td.closed { background: #bae0ba }
+table.progress td :hover { background: none }
+p.percent { font-size: 10px; line-height: 2.4em; margin: 0.9em 0 0 }
+
+/* Styles for the roadmap view */
+ul.milestones { margin: 2em 0 0; padding: 0 }
+li.milestone { list-style: none; margin-bottom: 4em }
+.milestone .info { white-space: nowrap }
+.milestone .info h2 {
+ background: #f7f7f7;
+ border-bottom: 1px solid #d7d7d7;
+ margin: 0;
+}
+.milestone .info h2 :link, .milestone .info h2 :visited {
+ color: #000;
+ display: block;
+ border-bottom: none;
+}
+.milestone .info h2 :link:hover, .milestone .info h2 :visited:hover {
+ color: #000;
+}
+.milestone .info h2 em { color: #b00; font-style: normal }
+.milestone .info .date {
+ color: #888;
+ font-size: 11px;
+ font-style: italic;
+ margin: 0;
+}
+.milestone .info .progress { margin: 1em 1em 0; width: 40em; max-width: 70% }
+.milestone .info dl {
+ font-size: 10px;
+ font-style: italic;
+ margin: 0 1em 2em;
+ white-space: nowrap;
+}
+.milestone .info dt { display: inline; margin-left: .5em }
+.milestone .info dd { display: inline; margin: 0 1em 0 .5em }
+.milestone .description { margin-left: 1em }
+
+/* Styles for the milestone view */
+.milestone .date { color: #888; font-style: italic; margin: 0 }
+.milestone .description { margin: 1em 0 2em }
+
+/* Styles for the milestone statistics table */
+#stats { float: right; margin: 0 0 2em 2em; width: 400px; max-width: 40% }
+#stats legend { white-space: nowrap }
+#stats table { border-collapse: collapse; width: 100% }
+#stats th, #stats td { font-size: 10px; padding: 0; white-space: nowrap }
+#stats th { text-align: right; text-transform: capitalize }
+#stats th :link, #stats th :visited { border: none }
+#stats td { padding-left: 0.5em; width: 100% }
+#stats td table.progress { margin: 3px 4px 3px 0 }
+#stats td table.progress td { padding: 0 }
+#stats td p.percent { line-height: 1.2em; margin-top: 3px }
+
+/* Styles for the milestone edit form */
+#edit fieldset { margin: 1em 0 }
+#edit em { color: #888; font-size: smaller }
+#edit .disabled em { color: #d7d7d7 }
+#edit .field { margin-top: 1.3em }
+#edit label { padding-left: .2em }
+#edit textarea#description { width: 97% }
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/css/search.css
@@ -0,0 +1,14 @@
+#content form { margin: 1em 0 0 }
+#content form p { margin: .5em 0 }
+#content hr { clear: left; margin-bottom: 0 }
+#notfound { margin: 2em; font-size: 110% }
+
+#results { margin-right: 3em }
+#results dt { margin: 1.5em 0 0 }
+#results dt a { color: #33c }
+#results dd { font-size: 80%; margin: 0; padding: 0 }
+#results .author, #results .date, #results .keywords { color: #090 }
+
+#quickjump { font-style: italic; font-weight: bold; }
+
+#paging { margin: 0 0 2em; text-align: center }
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/css/ticket.css
@@ -0,0 +1,92 @@
+@import url(code.css);
+
+#content { width: 700px; max-width: 100% }
+
+#newticket #description { width: 100% }
+#newticket #properties { width: 100% }
+
+#ticket {
+ background: #ffd;
+ border: 1px outset #996;
+ margin-top: 1em;
+ padding: .5em 1em;
+ position: relative;
+}
+h1 .status { color: #444; text-transform: lowercase; }
+#ticket h2.summary { margin: 0 0 .8em 0 }
+#ticket .date { color: #996; float: right; font-size: 85%; position: relative }
+#ticket .date p { margin: 0 }
+
+#ticket table.properties {
+ border-bottom: 1px solid #dd9;
+ border-top: 1px solid #dd9;
+ border-collapse: collapse;
+ table-layout: fixed;
+ width: 100%;
+}
+#ticket table.properties tr { border-bottom: 1px dotted #eed }
+#ticket table.properties td, #ticket table.properties th {
+ font-size: 80%;
+ padding: .5em 1em;
+ vertical-align: top;
+}
+#ticket table.properties th {
+ color: #663;
+ font-weight: normal;
+ text-align: left;
+ width: 20%;
+}
+#ticket table.properties td { width: 30% }
+#ticket table.properties .description { border-top: 1px solid #dd9 }
+
+#ticket .description form { 
+ float: right;
+ position: relative;
+ bottom: 1.8em;
+}
+
+#changelog { border: 1px outset #996; padding: 1em }
+#changelog h3 {
+ border-bottom: 1px solid #d7d7d7;
+ color: #999;
+ font-size: 100%;
+ font-weight: normal;
+}
+#changelog .threading {
+ float: right;
+ position: relative;
+ bottom: 0.3em;
+}
+#changelog h3 .threading form { 
+ display: inline
+}
+
+#changelog .changes { list-style: square; margin-left: 2em; padding: 0 }
+#changelog .comment { margin-left: 2em }
+
+form .field { margin-top: .75em; width: 100% }
+form #comment { width: 100% }
+
+#properties { white-space: nowrap; line-height: 160%; padding: .5em }
+#properties table { border-spacing: 0; width: 100%; }
+#properties table th {
+ padding: .4em;
+ text-align: right;
+ width: 20%;
+ vertical-align: top;
+}
+#properties table th.col2 { border-left: 1px dotted #d7d7d7 }
+#properties table td { vertical-align: middle; width: 30% }
+#properties table td.fullrow { vertical-align: middle; width: 80% }
+
+#action { line-height: 2em }
+
+fieldset.radio { border: none; margin: 0; padding: 0 }
+fieldset.radio legend {
+ color: #000;
+ float: left;
+ font-size: 100%;
+ font-weight: normal;
+ padding: 0 1em 0 0;
+}
+fieldset.radio label { padding-right: 1em }
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/css/timeline.css
@@ -0,0 +1,70 @@
+/* Timeline */
+
+* html #prefs { width: 34em } /* Set width only for IE */
+#prefs fieldset label { display: block }
+#prefs .buttons { margin-top: -1.6em }
+
+h2 {
+ background: #f7f7f7;
+ border-bottom: 1px solid #d7d7d7;
+ font-size: 105%;
+ margin: 2em 0 .5em;
+}
+dl { line-height: 1.3em; margin-left: 1em }
+dt { background: 3px 4px no-repeat; padding: 0 }
+dt :link, dt :visited {
+ background: 3px 3px no-repeat;
+ border: none;
+ color: #000;
+ padding: 0 4px 2px 22px;
+}
+dt>:link, dt>:visited {
+ /* Hide from IE/Win */
+ background-position: 3px 4px;
+ display: block;
+}
+dt :link:hover, dt :visited:hover { background-color: #eed; color: #000 }
+dt em {
+ border-bottom: 1px dotted #bbb;
+ color: #b00;
+ font-style: normal;
+ text-decoration: none;
+}
+dt .time { color: #999; font-size: 80%; }
+dd { 
+ font-size: 80%;
+ margin: 0 0 .75em 5.5em;
+ padding: 0;
+ color: #776;
+}
+
+/* Apply icon background-image twice to avoid hover-flicker in IE/Win */
+dt.changeset, dt.changeset a { background-image: url(../changeset.png) !important }
+dt.newticket, dt.newticket a { background-image: url(../newticket.png) !important }
+dt.editedticket, dt.editedticket a { background-image: url(../editedticket.png) !important }
+dt.closedticket, dt.closedticket a { background-image: url(../closedticket.png) !important }
+dt.wiki, dt.wiki a { background-image: url(../wiki.png) !important }
+dt.milestone, dt.milestone a { background-image: url(../milestone.png) !important }
+dt.attachment, dt.attachment a { background-image: url(../attachment.png) !important }
+
+/* styles for the 'changeset_long_messages' option */
+dd.changeset p { margin: 0; padding: 0 }
+dd.changeset ul { padding-left: 15px; }
+
+/* Styles for the 'changeset_show_files' option */
+dd.changeset ul.changes { 
+ padding-left: 0;
+ list-style-type: none;
+}
+dd.changeset .changes li div {
+ border: 1px solid #999;
+ float: left;
+ margin: .4em .5em 0 0;
+ overflow: hidden;
+ width: .8em; height: .8em;
+}
+dd.changeset .changes .add     { background: #bfb }
+dd.changeset .changes .delete  { background: #f88 }
+dd.changeset .changes .edit    { background: #fd8 }
+dd.changeset .changes .copy    { background: #88f }
+dd.changeset .changes .move    { background: #ccc }
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/css/trac.css
@@ -0,0 +1,450 @@
+/* Trac CSS */
+body {
+ background: #fff;
+ color: #000;
+ margin: 10px;
+ padding: 0;
+}
+body, th, td {
+ font: normal 13px verdana,arial,'Bitstream Vera Sans',helvetica,sans-serif;
+}
+h1, h2, h3, h4 {
+ font-family: arial,verdana,'Bitstream Vera Sans',helvetica,sans-serif;
+ font-weight: bold;
+ letter-spacing: -0.018em;
+}
+h1 { font-size: 19px; margin: .15em 1em 0 0 }
+h2 { font-size: 16px }
+h3 { font-size: 14px }
+hr { border: none;  border-top: 1px solid #ccb; margin: 2em 0 }
+address { font-style: normal }
+img { border: none }
+
+.underline { text-decoration: underline }
+ol.loweralpha { list-style-type: lower-alpha }
+ol.upperalpha { list-style-type: upper-alpha }
+ol.lowerroman { list-style-type: lower-roman }
+ol.upperroman { list-style-type: upper-roman }
+ol.arabic     { list-style-type: decimal }
+
+/* Link styles */
+:link, :visited {
+ text-decoration: none;
+ color: #b00;
+ border-bottom: 1px dotted #bbb;
+}
+:link:hover, :visited:hover {
+ background-color: #eee;
+ color: #555;
+}
+h1 :link, h1 :visited ,h2 :link, h2 :visited, h3 :link, h3 :visited,
+h4 :link, h4 :visited, h5 :link, h5 :visited, h6 :link, h6 :visited {
+ color: inherit;
+}
+
+/* Heading anchors */
+.anchor:link, .anchor:visited {
+ border: none;
+ color: #d7d7d7;
+ font-size: .8em;
+ vertical-align: text-top;
+}
+* > .anchor:link, * > .anchor:visited {
+ visibility: hidden;
+}
+h1:hover .anchor, h2:hover .anchor, h3:hover .anchor,
+h4:hover .anchor, h5:hover .anchor, h6:hover .anchor {
+ visibility: visible;
+}
+
+@media screen {
+ a.ext-link .icon {
+  background: url(../extlink.gif) left center no-repeat;
+  padding-left: 16px;
+ }
+ * html a.ext-link .icon { display: inline-block; }
+}
+
+/* Forms */
+input, textarea, select { margin: 2px }
+input, select { vertical-align: middle }
+input[type=button], input[type=submit], input[type=reset] {
+ background: #eee;
+ color: #222;
+ border: 1px outset #ccc;
+ padding: .1em .5em;
+}
+input[type=button]:hover, input[type=submit]:hover, input[type=reset]:hover {
+ background: #ccb;
+}
+input[type=button][disabled], input[type=submit][disabled],
+input[type=reset][disabled] {
+ background: #f6f6f6;
+ border-style: solid;
+ color: #999;
+}
+input[type=text], input.textwidget, textarea { border: 1px solid #d7d7d7 }
+input[type=text], input.textwidget { padding: .25em .5em }
+input[type=text]:focus, input.textwidget:focus, textarea:focus {
+ border: 1px solid #886;
+}
+option { border-bottom: 1px dotted #d7d7d7 }
+fieldset { border: 1px solid #d7d7d7; padding: .5em; margin: 0 }
+fieldset.iefix { background: transparent; border: none; padding: 0; margin: 0 }
+* html fieldset.iefix { width: 98% }
+fieldset.iefix p { margin: 0 }
+legend { color: #999; padding: 0 .25em; font-size: 90%; font-weight: bold }
+label.disabled { color: #d7d7d7 }
+.buttons { margin: .5em .5em .5em 0 }
+.buttons form, .buttons form div { display: inline }
+.buttons input { margin: 1em .5em .1em 0 }
+.inlinebuttons input { 
+ font-size: 70%;
+ border-width: 1px;
+ border-style: dotted;
+ margin: 0;
+ padding: 0.1em;
+ background: none;
+}
+
+/* Header */
+#header hr { display: none }
+#header h1 { margin: 1.5em 0 -1.5em; }
+#header img { border: none; margin: 0 0 -3em }
+#header :link, #header :visited, #header :link:hover, #header :visited:hover {
+ background: transparent;
+ color: #555;
+ margin-bottom: 2px;
+ border: none;
+}
+#header h1 :link:hover, #header h1 :visited:hover { color: #000 }
+
+/* Quick search */
+#search {
+ clear: both;
+ font-size: 10px;
+ height: 2.2em;
+ margin: 0 0 1em;
+ text-align: right;
+}
+#search input { font-size: 10px }
+#search label { display: none }
+
+/* Navigation */
+.nav h2, .nav hr { display: none }
+.nav ul { font-size: 10px; list-style: none; margin: 0; text-align: right }
+.nav li {
+ border-right: 1px solid #d7d7d7;
+ display: inline;
+ padding: 0 .75em;
+ white-space: nowrap;
+}
+.nav li.last { border-right: none }
+
+/* Main navigation bar */
+#mainnav {
+ background: #f7f7f7 url(../topbar_gradient.png) 0 0;
+ border: 1px solid #000;
+ font: normal 10px verdana,'Bitstream Vera Sans',helvetica,arial,sans-serif;
+ margin: .66em 0 .33em;
+ padding: .2em 0;
+}
+#mainnav li { border-right: none; padding: .25em 0 }
+#mainnav :link, #mainnav :visited {
+ background: url(../dots.gif) 0 0 no-repeat;
+ border-right: 1px solid #fff;
+ border-bottom: none;
+ border-left: 1px solid #555;
+ color: #000;
+ padding: .2em 20px;
+}
+* html #mainnav :link, * html #mainnav :visited { background-position: 1px 0 }
+#mainnav :link:hover, #mainnav :visited:hover {
+ background-color: #ccc;
+ border-right: 1px solid #ddd;
+}
+#mainnav .active :link, #mainnav .active :visited {
+ background: #333 url(../topbar_gradient2.png) 0 0 repeat-x;
+ border-top: none;
+ border-right: 1px solid #000;
+ color: #eee;
+ font-weight: bold;
+}
+#mainnav .active :link:hover, #mainnav .active :visited:hover {
+ border-right: 1px solid #000;
+}
+
+/* Context-dependent navigation links */
+#ctxtnav { height: 1em }
+#ctxtnav li ul {
+ background: #f7f7f7;
+ color: #ccc;
+ border: 1px solid;
+ padding: 0;
+ display: inline;
+ margin: 0;
+}
+#ctxtnav li li { padding: 0; }
+#ctxtnav li li :link, #ctxtnav li li :visited { padding: 0 1em }
+#ctxtnav li li :link:hover, #ctxtnav li li :visited:hover {
+ background: #bba;
+ color: #fff;
+}
+
+/* Alternate links */
+#altlinks { clear: both; text-align: center }
+#altlinks h3 { font-size: 12px; letter-spacing: normal; margin: 0 }
+#altlinks ul { list-style: none; margin: 0; padding: 0 0 1em }
+#altlinks li {
+ border-right: 1px solid #d7d7d7;
+ display: inline;
+ font-size: 11px;
+ line-height: 16px;
+ padding: 0 1em;
+ white-space: nowrap;
+}
+#altlinks li.last { border-right: none }
+#altlinks li :link, #altlinks li :visited {
+ background-position: 0 -1px;
+ background-repeat: no-repeat;
+ border: none;
+}
+#altlinks li a.ics { background-image: url(../ics.png); padding-left: 22px }
+#altlinks li a.rss { background-image: url(../xml.png); padding-left: 42px }
+
+/* Footer */
+#footer {
+  clear: both;
+  color: #bbb;
+  font-size: 10px;
+  border-top: 1px solid;
+  height: 31px;
+  padding: .25em 0;
+}
+#footer :link, #footer :visited { color: #bbb; }
+#footer hr { display: none }
+#footer #tracpowered { border: 0; float: left }
+#footer #tracpowered:hover { background: transparent }
+#footer p { margin: 0 }
+#footer p.left {
+  float: left;
+  margin-left: 1em;
+  padding: 0 1em;
+  border-left: 1px solid #d7d7d7;
+  border-right: 1px solid #d7d7d7;
+}
+#footer p.right {
+  float: right;
+  text-align: right;
+}
+
+#content { padding-bottom: 2em; position: relative }
+
+#help {
+ clear: both;
+ color: #999;
+ font-size: 90%;
+ margin: 1em;
+ text-align: right;
+}
+#help :link, #help :visited { cursor: help }
+#help hr { display: none }
+
+/* Page preferences form */
+#prefs {
+ background: #f7f7f0;
+ border: 1px outset #998;
+ float: right;
+ font-size: 9px;
+ padding: .8em;
+ position: relative;
+ margin: 0 1em 1em;
+}
+* html #prefs { width: 26em } /* Set width only for IE */
+#prefs input, #prefs select { font-size: 9px; vertical-align: middle }
+#prefs fieldset {
+ background: transparent;
+ border: none;
+ margin: .5em;
+ padding: 0;
+}
+#prefs fieldset legend {
+ background: transparent;
+ color: #000;
+ font-size: 9px;
+ font-weight: normal;
+ margin: 0 0 0 -1.5em;
+ padding: 0;
+}
+#prefs .buttons { text-align: right }
+
+/* Version information (browser, wiki, attachments) */
+#info {
+ margin: 1em 0 0 0;
+ background: #f7f7f0;
+ border: 1px solid #d7d7d7;
+ border-collapse: collapse;
+ border-spacing: 0;
+ clear: both;
+ width: 100%;
+}
+#info th, #info td { font-size: 85%; padding: 2px .5em; vertical-align: top }
+#info th { font-weight: bold; text-align: left; white-space: nowrap }
+#info td.message { width: 100% }
+#info .message ul { padding: 0; margin: 0 2em }
+#info .message p { margin: 0; padding: 0 }
+
+/* Wiki */
+.wikipage { padding-left: 18px }
+.wikipage h1, .wikipage h2, .wikipage h3 { margin-left: -18px }
+
+a.missing:link, a.missing:visited, span.missing { color: #998 }
+a.missing:link, a.missing:visited { background: #fafaf0 }
+a.missing:hover { color: #000 }
+a.closed:link, a.closed:visited { text-decoration: line-through }
+
+dl.wiki dt { font-weight: bold }
+dl.compact dt { float: left; padding-right: .5em }
+dl.compact dd { margin: 0; padding: 0 }
+
+pre.wiki, pre.literal-block {
+ background: #f7f7f7;
+ border: 1px solid #d7d7d7;
+ margin: 1em 1.75em;
+ padding: .25em;
+ overflow: auto;
+}
+
+blockquote.citation { 
+ margin: -0.6em 0;
+ border-style: solid; 
+ border-width: 0 0 0 2px; 
+ padding-left: .5em;
+ border-color: #b44; 
+}
+.citation blockquote.citation { border-color: #4b4; }
+.citation .citation blockquote.citation { border-color: #44b; }
+.citation .citation .citation blockquote.citation { border-color: #c55; }
+
+table.wiki {
+ border: 2px solid #ccc;
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+table.wiki td { border: 1px solid #ccc;  padding: .1em .25em; }
+
+.wikitoolbar {
+ border: solid #d7d7d7;
+ border-width: 1px 1px 1px 0;
+ height: 18px;
+ width: 208px;
+}
+.wikitoolbar :link, .wikitoolbar :visited {
+ background: transparent url(../edit_toolbar.png) no-repeat;
+ border: 1px solid #fff;
+ border-left-color: #d7d7d7;
+ cursor: default;
+ display: block;
+ float: left;
+ width: 24px;
+ height: 16px;
+}
+.wikitoolbar :link:hover, .wikitoolbar :visited:hover {
+ background-color: transparent;
+ border: 1px solid #fb2;
+}
+.wikitoolbar a#em { background-position: 0 0 }
+.wikitoolbar a#strong { background-position: 0 -16px }
+.wikitoolbar a#heading { background-position: 0 -32px }
+.wikitoolbar a#link { background-position: 0 -48px }
+.wikitoolbar a#code { background-position: 0 -64px }
+.wikitoolbar a#hr { background-position: 0 -80px }
+.wikitoolbar a#np { background-position: 0 -96px }
+.wikitoolbar a#br { background-position: 0 -112px }
+
+/* Styles for the form for adding attachments. */
+#attachment .field { margin-top: 1.3em }
+#attachment label { padding-left: .2em }
+#attachment fieldset { margin-top: 2em }
+#attachment fieldset .field { float: left; margin: 0 1em .5em 0 }
+#attachment .options { float: left; padding: 0 0 1em 1em }
+#attachment br { clear: left }
+.attachment #preview { margin-top: 1em }
+
+/* Styles for the list of attachments. */
+#attachments { border: 1px outset #996; padding: 1em }
+#attachments .attachments { margin-left: 2em; padding: 0 }
+#attachments dt { display: list-item; list-style: square; }
+#attachments dd { font-style: italic; margin-left: 0; padding-left: 0; }
+
+
+/* Styles for tabular listings such as those used for displaying directory
+   contents and report results. */
+table.listing {
+ clear: both;
+ border-bottom: 1px solid #d7d7d7;
+ border-collapse: collapse;
+ border-spacing: 0;
+ margin-top: 1em;
+ width: 100%;
+}
+table.listing th { text-align: left; padding: 0 1em .1em 0; font-size: 12px }
+table.listing thead { background: #f7f7f0 }
+table.listing thead th {
+ border: 1px solid #d7d7d7;
+ border-bottom-color: #999;
+ font-size: 11px;
+ font-weight: bold;
+ padding: 2px .5em;
+ vertical-align: bottom;
+}
+table.listing thead th :link:hover, table.listing thead th :visited:hover {
+ background-color: transparent;
+}
+table.listing thead th a { border: none; padding-right: 12px }
+table.listing th.asc a, table.listing th.desc a { font-weight: bold }
+table.listing th.asc a, table.listing th.desc a {
+ background-position: 100% 50%;
+ background-repeat: no-repeat;
+}
+table.listing th.asc a { background-image: url(../asc.png) }
+table.listing th.desc a { background-image: url(../desc.png) }
+table.listing tbody td, table.listing tbody th {
+ border: 1px dotted #ddd;
+ padding: .33em .5em;
+ vertical-align: top;
+}
+table.listing tbody td a:hover, table.listing tbody th a:hover {
+ background-color: transparent;
+}
+table.listing tbody tr { border-top: 1px solid #ddd }
+table.listing tbody tr.even { background-color: #fcfcfc }
+table.listing tbody tr.odd { background-color: #f7f7f7 }
+table.listing tbody tr:hover { background: #eed !important }
+
+/* Styles for the error page (and rst errors) */
+#content.error .message, div.system-message {
+ background: #fdc;
+ border: 2px solid #d00;
+ color: #500;
+ padding: .5em;
+ margin: 1em 0;
+}
+#content.error pre, div.system-message pre { margin-left: 1em; overflow: auto }
+div.system-message p { margin: 0; }
+div.system-message p.system-message-title { font-weight: bold; }
+
+/* Styles for search word highlighting */
+@media screen {
+ .searchword0 { background: #ff9 }
+ .searchword1 { background: #cfc }
+ .searchword2 { background: #cff }
+ .searchword3 { background: #ccf }
+ .searchword4 { background: #fcf }
+}
+
+@media print {
+ #header, #altlinks, #footer, #help { display: none }
+ .nav, form, .buttons form, form .buttons { display: none }
+ form.printableform { display: block }
+}
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/css/wiki.css
@@ -0,0 +1,53 @@
+@import url(code.css);
+
+/* Styles for the page editing form */
+#edit #rows { float: right; font-size: 80% }
+#edit #rows select { font-size: 90% }
+#edit #text { clear: both; width: 100% }
+#edit .wikitoolbar { float: left; }
+#changeinfo { padding: .5em }
+#changeinfo .field { float: left; margin: 0 1em .5em 0 }
+#changeinfo br { clear: left }
+#changeinfo .options { padding: 0 0 1em 1em }
+#changeinfo .options, #changeinfo .buttons { clear: left }
+#delete { margin-left: 6em }
+#preview {
+ background: #f4f4f4 url(../draft.png);
+ margin: 1em 0 2em;
+ overflow: auto;
+}
+
+/* Diff view */
+#overview .multi { color: #999 }
+#overview .ipnr { color: #999; font-size: 80% }
+#overview .comment { padding: 1em 0 0 }
+
+/* Styles for the page history table
+   (extends the styles for "table.listing") */
+#wikihist td { padding: 0 .5em }
+#wikihist td.date, #wikihist td.diff, #wikihist td.version,
+#wikihist td.author {
+ white-space: nowrap;
+}
+#wikihist td.version { text-align: center }
+#wikihist td.comment { width: 100% }
+
+@media print { 
+ th.diff, td.diff { display: none }
+}
+
+/* Styles for the TracGuideToc wikimacro */
+.wiki-toc {
+ padding: .5em 1em;
+ margin: 0 0 2em 1em;
+ float: right;
+ border: 1px outset #ddc;
+ background: #ffd;
+ font-size: 85%;
+ position: relative;
+}
+.wiki-toc h4 { font-size: 12px; margin: 0 }
+.wiki-toc ul, .wiki-toc ol { list-style: none; padding: 0; margin: 0 }
+.wiki-toc ul ul, .wiki-toc ol ol { padding-left: 1.2em }
+.wiki-toc li { margin: 0; padding: 0 }
+.wiki-toc .active { background: #ff9; position: relative; }
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..207018551f623aaa16ad5e52477f37e7c84c5590
GIT binary patch
literal 222
zc%17D@N?(olHy`uVBq!ia0vp^96-#@!VDyjmkZ_sDdu7)&kzm{j@u9Y9{{=h0X`wF
zuV25udiCm=GiUbi-@kI@%BvF}%mYfXmIV0)GyDevnG0$lvp5SpB8wRqxITa|qthCb
z89+hB64!{5;QX|b^2DN42H(WwqWs*{l*~j0x6Go{^8BLgVg+M8gZG(+KwS(Po-U3d
z6?2jUn3&i^#l_Xx(!|A?+1$j%8QIu`)7jYA1l<)5E;+!!;Cq^Rr|-_5g+M(Fp00i_
I>zopr0F_rm$N&HU
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..c6ae052feca96879d63b3046e7c6416029a4104f
GIT binary patch
literal 50
yc${<hbhEHbWMg1sXkcLQ0RzRKER0+Xj0`#qKmd|qVB+kNUyxAUB{7SY!5RSL`w0vH
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..cd6a67a558ed2bdcdb41198c87a22356ed9c5901
GIT binary patch
literal 4953
zc$~dhdsI_bv(Jlg0!c`MLI|%z5)dt7cqveo1VTs<5GW!lDv1KIq5?(>Ew&^?NHAbP
zrHHojh^<9bQ0#|&czHzS)fy0a2}-C^6!nJITC29+eGcEff8Mq3KR0W!$eBI!+q364
zGkZ?7@o`ZU8<q_QgP};o!bA)P%K?8}2oEG5y!rZT@Q;uwh!tQkm%3M3l|BZ~PKwAB
z#RvN`6dy@*(=mdb`(){`WREm6JuzLnGe4&%eKk-jjgLu+oSB)Kx;HUCGCDv1+l+bo
z&6_vl<Kx3acPB>2UcP+!?Af!iyTj(GsbB6txIZyDF*bH@Y<zNj{K5VEzs}4|KbU&-
z@X^T7-O=F@^Mk3mxw&cc^z7{HgUS12qhr%%^TS_$85$V8GdMVT@7|zk;O^j^$;ruy
zdlLhufuTD?Bf}%ZgLis*dY(Ld^zPleqPn*yfLFeX#JCUM{a-#9)zSSMFc^ljL?}qg
z8<&|}t<)Gk#!pAY3NSQ&gH*`G5KM9=oru8(vP5buA5&ILmx3h@M;E+wkOIE2b<9@p
z-@L$znkP#ek{XO>FjymxtrC~i^KlSdTtOEuvSJ{pdbpme$KW(l1A%U(B+&R|j+ds}
zj$4S4F+&8xgD{pr>w;|9xp)#>SbTs<YMG7a00Vj6uR$vt2>^rgyZ@<Wh-KGG9dl8&
z;P^Rj$In}v$*Vk3VUVuyMv#4gs;{@5vOYnV%B`^in{=(zDB^J>Z-SktltIA*m(0$>
zS*4+`3rpf8Fk%sj)J4ct8v6;+(NCMWENjFnM8m^LJ})`5MLPr!S|K{F!fK_Eia`Ke
zg-ImT?-_xx3_YgVO_&6sqL%_;K6(_39lI1c3RA?VC9uRuSQsizC#p`T3y&jFp`77W
z8XXrR1is5M2I~@lv5D8aJJ1%uvAm?lm_A@qYo?3HE?I$OMCc$TG!3N4nJDIJ5b7#4
zoLyXvLJ=%a&c=h?vq@~%PiyY(MO0E%;wW`SECzAUS3t1iJh4U?A%6}Jn!nn#g8_Uj
zUPpV(%N<;X#QT-Hf{MTJohM+u;J2D`%!StgB~aa^aayithU{7dwhdHtLE}*oHr%zW
zG8_hMNBcS5N3Jw>NKT>Wt4pQTSyu+{xp@pB9I~d1;1-@~fqrk~N!v2WMxVs!(_AH8
zOzwQO@f{L46P5jfKmCa9r{{L;#>}s%AOJ)UH$a;D`fx=&JfGW4OJ$S5i6ho%OgPex
z3!FN>3X@;35(ljHcI@N)#P-zgs2G?uJ$amr=!I#flt!aR`GtD6i<nS#6^RfJVyD6L
zQ_n{I5*7)+L*nG3f_Bj=b$oVP2o8x!1}z8bCE=CiFizETpIu5}-a82J^bIdjJv<Ua
zw$z_IR^pzLA3zU<a38_+mK#KnxJk^G%SF9z*jD11R)V|vihs5h!|!_}uJRL6?R>^@
z_I_cxb^=MNS`T{>xwQ7}jYi4K`DLe{?j%<D0{jy^oWscIpPjs8RUh<>*`o*b(lqa7
z%972rW877G%_JQaF3~hwRbnKk!V>UW`61*4Y8A8&&e4AxQFTIl`p{KGbX)}m=&Qvq
zeCrQ#q;q}<@87_k5Z5hnjZe(Y=UGTnY!;~9#LfK?t)F{#kG4}0Y#)sYFVVCrI(mg4
z(;g#%jkjgD@~64aAn&!g<pQJ*JRF69^Yw`y{p)|0(2>nvsGuOdP~6ymvO6ROkHj#I
zR!69WB+K10{HS{qj}DrUTB>6TyOmt=;VvVDCS|2S4RPbBt~CpJ`4g<NX4nN$?Lof=
zDWW0Lee|ZI_6?CpX{1p&83L7kiJl)Gwsf8D#FM>PsJ4S8!U<#eU3hK9cA;A9_@)tB
z29Q}zQe$020W2tfd-8Dm-(;&N-&?_j#NEuCqF*(~nOYJzFH={dEjo4b^}kWb6E3wO
zY+tWLoK}PZs-K*x%BP{d=XFbkfQF?0t6QVQwFsY(?k+VCaVehpyZbCsq#k}B1-2u#
z7_ozzn}+tW?$nosM8y6?5FPJ?*zX)3CZ{20rcDh&*{HVM^<E*`0?uCj6u%fzG7Lvh
z;}NA=h?C*lNMr;!F4^z}(wU0b`IgfCiA(ftCl6dB<b9LN4n!*u?h&|zH-Ww+qHfDA
ze|pOn37u(?e@r5fv0pY+*dT&tLPa^%HYZ@-NMKlRuthsopn$-7xTG<qJ}->*4LCHD
zm_So9Fm~@P?&U7~#0C*;pebiIIeAmKjq5X!+JSA^X)Q)$WCEP<*-dvuV+&1L>bWWI
zH@ud3gpKTEv7#s3V0gY65L+Rc!7S2L%e1P$<pvzG=V^gD{d<ld(Ujrpj8HxZ<MON9
z9pYm+@>VybArbe`l;!){xBJvNhc}?bg0<nq)ek#j-^`PZ){F}(u_NGT*`;}!qli%I
znc)>mK&uDTUn$*5+%6-nFWxazdk9HhQ{mm+<R6U0N~-;rcZ3h+?nu1{W<TTj?sbsi
zwTGR5y)Gk-Tch?nx2vP{vj*dPg@<08Xj<@>Wm>y@i92VY&ZxYaGF=yeVq%WE=lHe6
zC;9jdcxV(@oV@+khi9u5niW;NaqL?K#NG-FU-^BO>_MGA;`yQ@mAH4?^ldHd2rkva
z;)+o2pFCUI11kT_4q5tFKL!8*ewVnCzWJMVxpTDbK9YfZCxMgA`LUR)Pg3$Vx3l#f
zsk=x{zM<DZe26P`ZF%NvB_CA1BHwz?8y70oeRoCP2CisI_DRZpmDTbC5_w((_6r7*
zR*E+odj94UC0YZ_*m3qK2kccl_vzZ>4Yr$r)*LT-u%`^{Ws(w{vLcTA7+3w^^ms-A
z+_$Xcy1#a+Cd-O^7LM2j)#$4wS$TK-(C4W)>~QAi{j>qgSD9!~;Mq%l(y_SMT663O
z%9@~g_^5S&2R0%u-?TuGiq~1AA59^~q6TRx8~gB7yRSM}C@0>-nfn{I`{PM$(T1$_
zRg*<1%aKgK=Gtw^5QR<5ij97~KvemUv~wg9T?kv(zf@Smh(DU)<jc8O>@E_?Q=>@u
zx|+yFrBw$n`-?3_HT^y=G4)MCT?K#dM|?ZBzg_%UHt1vHn|hZTg%1k?70r}$5VNvh
zSFP;e)d~!@G=A&l#XBq~tlq74?9Pv>EnIYqOqMfS#1t{w6U#a^M@|{<{Y)c~MtdUH
zuV9G(H9*E7kZQUer)OiQw#&YfCaRLLOz04?SxrA5T%PQIeK((0w5MrsHWywh`VzbQ
zBq(4?Cv?w>JbGy?0$b3w;&es&0Ntg%*oBWtc<^Dy>R&WIWFwc`uU@*WHzXT%Ey<4a
z_Pva2J?q4Hq}G)e1%|G^7`aYely5pr2SsHH(pdIQ8$(A|LdaNWTBT9!RFFKm+b(uE
zh%i{;Brnk`_>A&T{}6GwjFlw?o#;heR9=_ng!8qQ?%o?A!JQCXD35pZvx&BtPw!dm
zA=9Z3Q${__l~coJrc1*X0J3Hy@4Qm7*e!1B>N)L0i!<)WjzZGM!b{ztae+dw-DVU1
zEZ*FGu$~b5@wrCps2#{ebgsHb193&TNb-{C%<7!%q#V#9_&>sIFa0Z$6IcA=!j%_z
zM05jYv-QyH&D6hNP79FBKv^RY^7O&AUn`Gf@6WcZOY4(#7Og;8RyGQR-g<sI5C1eN
zzv<Qy$nH8Cbm9{t{U1Kgi!&sN?5~iRI~>v5p8p8$bQTvhh0BZDVk|*r{rag&rvfyx
z<RL11wbH0g_rio8{Hgz^zCP-ImXR+mpfw@`OULF3pK+JqjmmDm_0oKz6wL<wA4SmQ
z(dKUhY;B*P^`9^?zSu=WZ*)HstT7ezL>nvORC;Bqg}c1L1Uoxp0orB)eKV}3g;G_v
zp)u#r_fdVW5+_@}MEpAjhf}!WQbQN?`1$_AOn!>>mW6HhsGtyiigxn!&ARu4u?W1k
zMvs$-Gc_fE_6k?gX+lYQost{-Vx`kR0v1B`L8jKeMM`%OH$62L(;vVTzzutqE=4<b
zPrFDw!G24)b8Pm%rS14)pP08kLaWN!MRI9{&p+|9hb{&rJ$8age4B#_8<(g9_s7tm
zm3e(Ur2ws5q2NcN6MIJ*_u@fLiX-*<Uz?bq4tkxh4%u+W7K&SVxH#+pPJ8NrsTlZ~
z(6ArO&N7)>>q0g^<|n#|b*V+GP%=Zm$cn~Gvg5a#v0yFNWtY;xQ~R@Mpir)|^#;By
zJ!<dAmo@vTPf%7GJ##B@q{l;Khv$ONHc$cjZKc7Aao*c~m9_kG^5&0B_(Dv*j9cj-
zj(`A0kp1gy3b_30Ka}!fkbC-E!Iqw8WVmK8*HvFLJbHAkt$Hb$n|#q@F%rK5rlgOQ
z%inmzk_$zPuT(A;vzop;T`=MEv?;<{2lBF;H*HLf?O)D`6~T`!N3zK?Ih(S;U8=F+
zWMD;4GaLN%ba7zY+aos-A;d{uOPyHK?XPzfyPRG?$Yx4P&0+h_oYe7UbXGVT6hfD#
z%v6!=vYKl>NZXAEjw^ZL&+2}$v;@DmOBpy+Bf((RVY+JVmY?C9G0`yPS``;$K=v(?
zWaHT>s-bRVln9dSZAjT{K$w<l@ECPRl5VR?Y4q^!7F^}CO1G{o#B{of^!qQ2b^Phy
z60J>GqA%Wg$pJ3RE~Z~$_9m?{+faYkZ0+Z>YFs}972UoywIN?q77t6m^isKrBNSN#
zP|=nlxt!t-aN}zILXvKcf&*dqoxL5G2)9h_%ds14*^HPz@MSO86Z}4D)XcRMKd%l0
zxl5CW0#Z_v1D8?$S}3sa3%dcAefp9KFv^2FR0fY?J(lWuH&@r5iY(4ur~&g@)>5T9
zCmFhJ`nOWO^SW@jrS@pf?{<GL0Lean%_US4YW|u4^FP`l&UMhH@uWjxMYI*)Dtqg!
zkcQGW&#uR7@rPn6PafH?L9Z4+T8KfUfO8&G-v=+dWMg?K!&zM4wF03@*$uNp_3D!C
zOz^6LZxINyU`iJcyz`_m=>dw^Bv?ZJkA+7qBjED-nEzQN&Bia)7fZK+_szUe+~lS1
zTamNn!JW!Lau?{bw12J86yDB~)duSGicxHmeUs5@ke3w!jyv;;o~|@@hrC4^a(Bv(
z!>jbKO17hs2s!_U1*kQ>K7hqRb3Z^<N+S<NY<{C{<;&a|-^+TeL6&gtOKiA?qFp<S
z7n;?@9|VbT&ipj%XEz6slG#=6Tq}k>ePk_pD>!poUrHiQQf>LeBZ3LC&TV@IP2AZP
z;)GaUuU@iNzmxcv*5f!9<j$%0H5Bfw`sz~)86l!`yNc!{*4vL5Q%ZIr=TIhAweb^O
zUfhz#6F~gpV}(hulGtVagdH9VMs#+{7JDag#GyEMchE07m$RmEk^`^Mwn-m&!nsiN
zJ{hz)`W=6_$H=HG{KE~}^KBLDKGKeie>7W&gW0|pJCLG!dU%>-kfNiVFl)VQCQNyU
z=0SZ_KsI3zLgz8|r$+1DaG4riV2Exs69zR43+WGO9KA?{#zxX`UakxbM<-0Nvr<Gd
zR4`E3Y40_Oa9K5KotWuwND-x?20d5SxNv;Y92?@vW?Xa7>NS8)+ii=Y`{{y1MB7kZ
zt&|&zL&q91nPal4sBl1`Kg>7WG_DBhL^owBqOczS`%f_cA4`Q;%%0`b*`(6W5e)c9
MBIAS?!_!p%1<e>T7ytkO
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..9a4b65c99417a91f4102a281abe2aeabdab05ead
GIT binary patch
literal 2231
zc$@*Z2uSydP)<h;3K|Lk000e1NJLTq00Arj002D%0{{R3<*rH00000mP)t-s0096I
z6A&958YCqnE-x)OIyXf}L{n8#)gTOOZEA;!hT~;etE#8U%Esm8;{E^pIX+7|000O=
zNkl<ZXx{CbU2GIZ9KiS5E6rW*+L{IorlsY@L?T@EK@%gkCcbE*ZK7aIkk%M6_&|%O
z1fuly5?+c?NQruf21qZKzLNOWs;CfTkE%fp?H0c(AMH}Ht-IZwzyJBz-I?p&K~s|Q
zz$Crhnf=e~Z~k91yIT!z$)erHx{Y-kYhmT^1}xo+9fc0ELH~*VJ}+aDQ#<Z7RMB&_
zyU$#Iz#a`tYSHSUWgt&~lL6NGcWcUbA1-@e5_I2==5}by-!qm9>&nq4?eTsS;9}zw
zrYCo)<sJlb`&zUIKQ@;F>#VlIQ;EC1Qs-yiYHx}T=l9n{^d-O=lpW}s2sNa;uR>}h
zJxhT#rrh*W(?cfxlc)}9<`Q5{XA}W<UwK4!eX>t0Ukcx{Qgf;{PRi<YyEHAf6j)Bx
z+}@&Dz-dFl#F8Y4#9VrB5vR5rsbG~~ztY!xFqpzAS_!b+TyH-beR6WrRD@{@YoaIg
z0<F^%_f%hR?o2VPXWeu<{Y4nn#Tq8w>bcq5tSVB6+d(TI_*mzt@Jh2ws5cNE^lnBk
zwPQzGFM}(uG69W+R|wXJ1hLYe&!;-a`$aNdqTqFH8>_YPiyZ=MoL_rHSX~mHqK7zk
zq1@GI0$Pv#rFkV5j+Hl#hjM-6^%E8z_UaL8njuXpDR7(kS$Pe|IvWUE^iV0ZgqK)7
zITiHY9Ex>yfMKOSQ>ni8yKz@>f^m{cjbUAot4T$e;Y2+x0sPAxV>Xaj7K$~~{WHT#
ze=4V|5B*+I-f}h=rwmjK#A;Eo@<FAX84qtXLk|_G28JNdOAo`c8~)~4>Ai9i^~*}$
zM6DEg|2*@h0p`9EmZhm!Ot@bNGdDB1kD==G{Ge$M=1v`DZOvg=Zt}mh$7Kdd)Gs`3
zF%zs3o{@67*80zL$4`y1!}Ek&KB)@lxkmCrGg+>*H&|*ABw_olyXMj=m(wQCou{oS
z&-s~8bM3T?8*8bo@Db7&__Htk53OYw4n6jWz{E0f`b!?BVOVcC>3|EP%x+Rl(#}Sp
z*(X9zC~}P5DMIJKI8rShKf8BQ6=vw2&>@mwix4FzAN5m27}g<@mbi4MMxhdEi#f<0
zSeWCEiJ<ouA12xD8$}gn7H^=MYotIN688lDEh<B?@<v+bQr*`}TZ&!F^S;wDf^~dY
zA4`eLDAQ;a!bMOx;qoy_mz*KOu%<-fDuTt`piyor^A{A3wbtk7BdfwpCV^p4f!$&b
zH}LP;^{{3;zbk>YoMS1LEgw|1Rfweuv&Q%|BH>Ae#m(PxT`ar$?-E!mL$nng^wv4q
zh?#J0Rlqe$f>|_9GwBSQkj<Cr(M!d#NRYTfu#zy}w<`0#%Xb7W-wfV%K^10MB#K><
zs4;Dr`Tsv+0H-(>teGo;RV{7z{Jj>>*jObzZ*_P^?Fu3>*qAXd$JEha@gwSWOk@<h
z1lH>%up&HdaqF!2Zp0VbwE%xe=@dipB3G~~%oi?nrcQ?+kvK1AJW~Q|kd(k0;3Knj
zUwdT~$i&IbeEypD9ADG42EKAfUgEx<RE0&7#rqO32sl<;7@-K(m|nt&hGd9fmF!St
z8f?`aoq{Db@9mJd(;*IDL?7Q?A`;<R=6G=|<ypetPv%uDbw`mb5N4ssl$7<NXj+Zc
zP%NwUa#5`1sylf*Fkc@L?j$oRmMSdLfj^s8Rr$rRoCF*%h86eyHmk||)j@H(Tlg;5
zPh?O;SXNQD>nUl<U!}52!VQ0s)U}5kFM_o#*cx`p%cRTGG+Rq32j8OY855M!j*U8J
z3pcgUqUkG8tLtHFQ8<=UYs`gX#pR9k5guqO`u$B=rQ!NpvXcjPo$&*|KsYOQ7$ST6
zPkU=jrd~4~3;vA5&tX_m^?93|&gD*z`Y#Z3--kbBx6T(1DQ+&Lf5E%c>}Jo53RdQZ
zAJ^r_qs*e8{eAS+x-%cE?uG{<%gzL0Vcaw<7_WrOg;>kX8y*PNba!vx3-S}(&3OSn
zw4P@P32WfS2bwM1b2tay<7RmAgRsewzJqe}!J=5CZGgE~fc4}8<~LR7kUMhk@D=3P
zY}o9E4jz`@67?7VTwqt2b23(JA!^U7s|`4-=<f)o-O1mMO`62Sm>4mE5Q8=XFt44U
z4Wsyj7s&{#Q=x*d7{hfmOL$n31vzf82=e#R7TpsLKeo<U{ank2ZwIcmTHU{GjKNns
z+J~Q6M|Yl7X2nWucsT=Kxea9diy2xU-gs{z7U{~M89l5$3vyEbl||Vx9s=e4K<Z#8
z^uk%NGAB|~ILka_b+;Z4&>h&D0Q+Ig=+W(VsB_w<yTRNK7mQ6I-+VCHJ04bIfx+|Z
zYcy+@ZS{2gOH-%S4PV11I0ANNQ}q!{pFOY@X5Sh)Oy6jC)Vqn1AM~x@HdvijW(dug
z82M4(XoP%<!eoD$V@1t{hxNqBq&&yOzd9U+de{Z_TdfW2;icSbSi8D$%PY(@57;ZV
zY%%JAR9!Hpb<`lMkJsuUU&ylC;1lbnBx;=elnzh0)$lMlTl6*Ub}Q^8RdCgt?@k-n
zNP^4fy}RJ5u?w*}eWxtM!emb<R{dh3PQ!TfOvXw%tr&DpEj+5*&34>r#w{nHoi3S9
z0)urDFT=~L(CI!hU8f!<T(COBz9){y=;PBebFo<a$|_BA|8mpHz@da2ho^z8zNa1)
z<lc85TH=Q?G*-|3Ytj9nX8=yXOl=*l&qVJwLqBs4J!$!GOBYk9n-OJpL>x?!vH(U4
z<nfQcJfDw6+WNGoZo!Yjb|eP3AQqf6Zb_`$|8P*O{12~JBW`yj1c?9u002ovPDHLk
FV1fhPHs1gM
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..50e6a8bc02dbb1db012d1976aff75cdd1deecf5e
GIT binary patch
literal 978
zc$@*!11<cCP)<h;3K|Lk000e1NJLTq000;O004jp0{{R3CY)L10001NP)t-s|NsB@
z{r~^>|Ni**`TO_x`S$ht{qpqi?(ppC?CR#|<=x-h)z{V0)6dMz%*M#Yz{0+|yt%l#
zx3#ylvaqhLudJ!7sHCZ^rKh8xp`MwUnU0c`jFgOtjEaPYgMNU0fPI01e0zF-fqHy-
z2P*ro00001VoOIv0Eh)0NB{r;32;bRa{vGf6951U69E94oEQKA00(qQO+^RR1``%9
z9yRBJj{pDy>`6pHR7l6wRtvYAAPfe1)LN=@UcJKFhT{MKk4x}TpSyS4+1Zx%2<FR+
zgn*2FOkX?zRC`DZKHF8P!R_1n=F5Em;Ihj%0CE?o!jTKuxZwC>YULtfTuQQSo<S)l
z7$?Ga!3mI@SKh_R@L-Yz=;a&Kah!lSa%sRjrTED20i-&B_f9L72%&}4lBomwuo&-H
zi}1MB*$(h-UD#Hn0l?=4;Oj;s;8UXBkw&rgVXb?FI@xFh3=3}8ov1MLSML|5G}Da_
zG+OXeJt$LJHXdlwc%SsiV?xIfO%cb#trx`XY9theKv8)ph)Y1PfB@jueYmR{>VRoa
zVw$x|P*rIy@K0%1gdj&nZWPh4dYox-0A^E-h8#d>*Zd5{9To$!O~;@agVt6m8Ct>p
zd_yGLDlk^8y<{j!4WJ%~oR<nA23n%~X5nFIN=2(VFBIA`(hX(a<{pSVJZbWTv!whd
zF{BVe|0HUV%Y|V0-VUw9`PrkU5Q2J&)_^IGh*G5g7HuJLo3=jJCitg6Q{E9M^{g#!
zXzT4${c%jEOP-q|Pd2qOS&G4=`RV{li;KxklBQXaE`Xx0$|57mmkrWdTj!@!nwflA
z-nOWntuCjmw97-Q&CR3hN^LrKpPyH}Ds0zv*4VDLBR6K&wB6%zzq`lX6=Q$IgNs+r
zStNJ2DG6ddWDjJOt;(`?Z8k8*{*bAuY}@APtUo5wdRBm6Jg=bdo4Ai?!@Pi{T$s(`
zo+FK=+)85&gi$B#7~N>}9tJ0$ii*e8#KpYiIEQ$Hh~dQNVX8-v1P}coCE#6y1>~BA
zu|IkufUXH4hK?H31@rqOo@-M*R)zYx`{SP@*r6W-!+jpd<uxzmH0iTE)nmu8p!;Jl
zqF9e3fb9dnk$BZb+J4OtSK@zjU=e`m_N(g%Fh>A81d1boNqYdW>Td$qjy3jw+xnje
z+84mZ;<_IJ`s|t5=XTyR@(5UcY8(aH{;?E%2N^t45Z1?3ApigX07*qoM6N<$f@+)O
ARsaA1
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..9d12a9107496c42238d5e88b28cc31e515a9f060
GIT binary patch
literal 241
zc%17D@N?(olHy`uVBq!ia0vp^JRr=%3?!FCJ6-`&VkNE-CC){ui6xo&c?uz!xv30>
zhWdu)`UWPKZ&^kGRj`+M`ns}TWn~rPG?=%NClV;cS>O>_%)r3)0fZTy)|kuy3bLd-
z`Z_W&Z0zU$lgJ9>iw5|Fxc>kD|NfD#pI=*n0w11MGNdt_Uf}G@|9&fw&sY-V7tG-B
z>_!@pW8mrH7*cU7IiZ2U(I7z}h>3}nZQ&M^tt##o{4R`@3l}<i*d!+>D{J@*EQ)2w
a5@RSV=9tQw!!85V%i!ti=d#Wzp$PzX`$GZ%
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d47e14ca58d68e5b3f66f6a21b3ea7211636c289
GIT binary patch
literal 90
zc${<hbhEHb<Yi!IIK;v*W5tw3%O>sIv-Z;U{r{1G;!hSv1_l-e9R?r(sbyf6w-C~C
iW^qtb;}P&^T6<N-BqQZ!<;;*sSq}}L2Pwf!4Aub7j3Pn+
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f35fc99b3e6281ebab6b8fc8bea5f21a91f830b3
GIT binary patch
literal 285
zc%17D@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPHF3h)VWWnlRK{rmU#@83Ur_Uzuh
zd)KdDKXc~Hkt0VA95^s@=FFCsmWqms;^N}}|NqNXN9zD}F_r}R1v5B2yO9Rua29w(
z7Bet#eE?xbr!^)sfP(BLp1!W^S6GFa_?ecfYKa1cDm`5sLn>}1Cj=OonVA_UBqU7O
zTmAjh(=-KV^Dpn7?iS;6U<-KiZY_g?vhLeW-abqbPu|7OWZB>mwTLIgMc|Cbd0*`-
zjNLIlI!wlwxKDTIH1N1OI;ticEZ(zc&kgny0YPSkg&!Gb$?W`fW~T9S83_pq-^-^@
bJBct9m+PL3-Ctt}bOeK^tDnm{r-UW|XWVVB
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f35fc99b3e6281ebab6b8fc8bea5f21a91f830b3
GIT binary patch
literal 285
zc%17D@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPHF3h)VWWnlRK{rmU#@83Ur_Uzuh
zd)KdDKXc~Hkt0VA95^s@=FFCsmWqms;^N}}|NqNXN9zD}F_r}R1v5B2yO9Rua29w(
z7Bet#eE?xbr!^)sfP(BLp1!W^S6GFa_?ecfYKa1cDm`5sLn>}1Cj=OonVA_UBqU7O
zTmAjh(=-KV^Dpn7?iS;6U<-KiZY_g?vhLeW-abqbPu|7OWZB>mwTLIgMc|Cbd0*`-
zjNLIlI!wlwxKDTIH1N1OI;ticEZ(zc&kgny0YPSkg&!Gb$?W`fW~T9S83_pq-^-^@
bJBct9m+PL3-Ctt}bOeK^tDnm{r-UW|XWVVB
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d26c06cfc3d84b08274b7971b850a60256c83998
GIT binary patch
literal 357
zc$@)c0h<1aP)<h;3K|Lk000e1NJLTq000mG000mO0{{R3C@l|D0001EP)t-s0001C
zUM7lxF`1P-mzkEcr9!{3O2@KJ&9P6jwX)T@UE05D-p7RE&5z{JnBvf$)78@D)v4#%
zv)kR;>fgTS<;d>k%H`+e>g&|)>Duw_-tzJ2@$>NW_44=j?fm}xOh+Io(CHZf0004W
zQchC<K<3zH00009a7bBm000W`000W`0Ya=am;e9(2XskIMF-Rc69Nt`7w;ww0001p
zNkl<Zc-m#o;TD1*3<Y2;DMgY*+JKev{!iIv)Zv`&zkTNprqq8H4!)Xs<JkMNnTZJO
zWXzyD6v!z-j3IbGml4&nZ_*J2SNgR=&Itf+Um)VO2*Fk$0e~2$AAAbF0zbg;;9rb@
z+YSKm-VFkC;I2FFx@ZA=)#p=JY8Y_roA9Ly*Cwdmge?&f6{)0400000NkvXXu0mjf
DK(wNz
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d26c06cfc3d84b08274b7971b850a60256c83998
GIT binary patch
literal 357
zc$@)c0h<1aP)<h;3K|Lk000e1NJLTq000mG000mO0{{R3C@l|D0001EP)t-s0001C
zUM7lxF`1P-mzkEcr9!{3O2@KJ&9P6jwX)T@UE05D-p7RE&5z{JnBvf$)78@D)v4#%
zv)kR;>fgTS<;d>k%H`+e>g&|)>Duw_-tzJ2@$>NW_44=j?fm}xOh+Io(CHZf0004W
zQchC<K<3zH00009a7bBm000W`000W`0Ya=am;e9(2XskIMF-Rc69Nt`7w;ww0001p
zNkl<Zc-m#o;TD1*3<Y2;DMgY*+JKev{!iIv)Zv`&zkTNprqq8H4!)Xs<JkMNnTZJO
zWXzyD6v!z-j3IbGml4&nZ_*J2SNgR=&Itf+Um)VO2*Fk$0e~2$AAAbF0zbg;;9rb@
z+YSKm-VFkC;I2FFx@ZA=)#p=JY8Y_roA9Ly*Cwdmge?&f6{)0400000NkvXXu0mjf
DK(wNz
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..69b9193d0dc7f6af133d644d854f071bfd5ea574
GIT binary patch
literal 347
zc%17D@N?(olHy`uVBq!ia0vp^0zk~i!VDy@FgET4Qq09po*^6@9Je3(KLB!d1AIbU
zfB*jd`Sa(uZ{I$7^5ovVd)u~cTe)(j@qdP<rl$Vzh_tk{a&Mo7rsiGxhAB3-DU0uj
z05$TL1o;L3hX4jBeSd1<DWDW*fk$L90|U1Z2s2)~TlWVjC|2ScQQ};bnpl#VpQjL#
znVZUBV5x6pqHkbQ*|lgcP=z8$MR0yvNqJ&XDuZuga#4P6YD#9Jf?H-$YI%N9cCmu7
zp27P}!w*2k(Vi}jAr*6yEvm&i@@7vrzGKPEq8_`T=d#W_fn%0z;ww(<zAJESrg6ID
zq$zV80(uoaPH=L_I`Swhn202E7&0;@o3}YQItz$28@fm~P3`UJ?ddryu`8_cAQuBq
Xrs1;%9M(NRYZyFT{an^LB{Ts5P+Na|
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d1e60d9777d71b171cc1049f7ed08a06275f81cc
GIT binary patch
literal 205
zc%17D@N?(olHy`uVBq!ia0vp^93afd3?%;@)Hwm9n2Vh}LpV4%Za?&Y0OYa-_=LFr
z|NsBYnKKrzRy6}fI14-?iy0WWg+Q3`(%rg0KtaV4*NBqf{Irtt#G+IN-^Api{M^)(
z%tQsZ%%art{G#k)1!Fyf_nC$tfQrRHik*v66H7Al^Atidb5j`%4fKtS^bJf{FYn+1
qs^Ir@aSW-Lll<cVL*pL-4hB62=D(A+=Dr6?F?hQAxvX<aXaWE@BR(7e
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/js/query.js
@@ -0,0 +1,271 @@
+function initializeFilters() {
+
+  // Bail early for Konqueror and IE5.2/Mac, which don't fully support dynamic
+  // creation of form controls
+  try {
+    var test = document.createElement("input");
+    test.type = "button";
+    if (test.type != "button") throw Error();
+  } catch (e) {
+    return;
+  }
+
+  // Removes an existing row from the filters table
+  function removeRow(button, propertyName) {
+    var tr = getAncestorByTagName(button, "tr");
+
+    var mode = null;
+    var selects = tr.getElementsByTagName("select");
+    for (var i = 0; i < selects.length; i++) {
+      if (selects[i].name == propertyName + "_mode") {
+        mode = selects[i];
+        break;
+      }
+    }
+    if (mode && (getAncestorByTagName(mode, "tr") == tr)) {
+      // Check whether there are more 'or' rows for this filter
+      var next = tr.nextSibling;
+      if (next && (next.className == propertyName)) {
+        function getChildElementAt(e, idx) {
+          e = e.firstChild;
+          var cur = 0;
+          while (cur <= idx) {
+            while (e && e.nodeType != 1) e = e.nextSibling;
+            if (cur++ == idx) break;
+            e = e.nextSibling;
+          }
+          return e;
+        }
+
+        var thisTh = getChildElementAt(tr, 0);
+        var nextTh = getChildElementAt(next, 0);
+        next.insertBefore(thisTh, nextTh);
+        nextTh.colSpan = 1;
+
+        thisTd = getChildElementAt(tr, 0);
+        nextTd = getChildElementAt(next, 1);
+        next.replaceChild(thisTd, nextTd);
+      }
+    }
+
+    var tBody = tr.parentNode;
+    tBody.deleteRow(tr.sectionRowIndex);
+    if (!tBody.rows.length) {
+        tBody.parentNode.removeChild(tBody);
+    }
+    
+    if (propertyName) {
+      var select = document.forms["query"].elements["add_filter"];
+      for (var i = 0; i < select.options.length; i++) {
+        var option = select.options[i];
+        if (option.value == propertyName) option.disabled = false;
+      }
+    }
+  }
+
+  // Initializes a filter row, the 'input' parameter is the submit
+  // button for removing the filter
+  function initializeFilter(input) {
+    var removeButton = document.createElement("input");
+    removeButton.type = "button";
+    removeButton.value = input.value;
+    if (input.name.substr(0, 10) == "rm_filter_") {
+      removeButton.onclick = function() {
+        var endIndex = input.name.search(/_\d+$/);
+        if (endIndex < 0) endIndex = input.name.length;
+        removeRow(removeButton, input.name.substring(10, endIndex));
+        return false;
+      }
+    } else {
+      removeButton.onclick = function() {
+        removeRow(removeButton);
+        return false;
+      }
+    }
+    input.parentNode.replaceChild(removeButton, input);
+  }
+
+  // Make the submit buttons for removing filters client-side triggers
+  var filters = document.getElementById("filters");
+  var inputs = filters.getElementsByTagName("input");
+  for (var i = 0; i < inputs.length; i++) {
+    var input = inputs[i];
+    if (input.type == "submit" && input.name
+     && input.name.match(/^rm_filter_/)) {
+      initializeFilter(input);
+    }
+  }
+
+  // Make the drop-down menu for adding a filter a client-side trigger
+  var addButton = document.forms["query"].elements["add"];
+  addButton.parentNode.removeChild(addButton);
+  var select = document.getElementById("add_filter");
+  select.onchange = function() {
+    if (select.selectedIndex < 1) return;
+
+    if (select.options[select.selectedIndex].disabled) {
+      // Neither IE nor Safari supported disabled options at the time this was
+      // written, so alert the user
+      alert("A filter already exists for that property");
+      return;
+    }
+
+    // Convenience function for creating a <label>
+    function createLabel(text, htmlFor) {
+      var label = document.createElement("label");
+      if (text) label.appendChild(document.createTextNode(text));
+      if (htmlFor) label.htmlFor = htmlFor;
+      return label;
+    }
+
+    // Convenience function for creating an <input type="checkbox">
+    function createCheckbox(name, value, id) {
+      var input = document.createElement("input");
+      input.type = "checkbox";
+      if (name) input.name = name;
+      if (value) input.value = value;
+      if (id) input.id = id;
+      return input;
+    }
+
+    // Convenience function for creating an <input type="radio">
+    function createRadio(name, value, id) {
+      var input = document.createElement("input");
+      input.type = "radio";
+      if (name) input.name = name;
+      if (value) input.value = value;
+      if (id) input.id = id;
+      return input;
+    }
+
+    // Convenience function for creating a <select>
+    function createSelect(name, options, optional) {
+      var e = document.createElement("select");
+      if (name) e.name = name;
+      if (optional) e.options[0] = new Option();
+      if (options) {
+        for (var i = 0; i < options.length; i++) {
+          var option;
+          if (typeof(options[i]) == "object") {
+            option = new Option(options[i].text, options[i].value);
+          } else {
+            option = new Option(options[i], options[i]);
+          }
+          e.options[e.options.length] = option;
+        }
+      }
+      return e;
+    }
+
+    var propertyName = select.options[select.selectedIndex].value;
+    var property = properties[propertyName];
+    var table = document.getElementById("filters").getElementsByTagName("table")[0];
+    var tr = document.createElement("tr");
+    tr.className = propertyName;
+
+    var alreadyPresent = false;
+    for (var i = 0; i < table.rows.length; i++) {
+      if (table.rows[i].className == propertyName) {
+        var existingTBody = table.rows[i].parentNode;
+        alreadyPresent = true;
+        break;
+      }
+    }
+
+    // Add the row header
+    var th = document.createElement("th");
+    th.scope = "row";
+    if (!alreadyPresent) {
+      th.appendChild(createLabel(property.label));
+    } else {
+      th.colSpan = 2;
+      th.appendChild(createLabel("or"));
+    }
+    tr.appendChild(th);
+
+    var td = document.createElement("td");
+    if (property.type == "radio" || property.type == "checkbox") {
+      td.colSpan = 2;
+      td.className = "filter";
+      if (property.type == "radio") {
+        for (var i = 0; i < property.options.length; i++) {
+          var option = property.options[i];
+          td.appendChild(createCheckbox(propertyName, option,
+            propertyName + "_" + option));
+          td.appendChild(document.createTextNode(" "));
+          td.appendChild(createLabel(option ? option : "none",
+            propertyName + "_" + option));
+        }
+      } else {
+        td.appendChild(createRadio(propertyName, "1", propertyName + "_on"));
+        td.appendChild(document.createTextNode(" "));
+        td.appendChild(createLabel("yes", propertyName + "_on"));
+        td.appendChild(createRadio(propertyName, "!1", propertyName + "_off"));
+        td.appendChild(document.createTextNode(" "));
+        td.appendChild(createLabel("no", propertyName + "_off"));
+      }
+      tr.appendChild(td);
+    } else {
+      if (!alreadyPresent) {
+        // Add the mode selector
+        td.className = "mode";
+        var modeSelect = createSelect(propertyName + "_mode",
+                                      modes[property.type]);
+        td.appendChild(modeSelect);
+        tr.appendChild(td);
+      }
+
+      // Add the selector or text input for the actual filter value
+      td = document.createElement("td");
+      td.className = "filter";
+      if (property.type == "select") {
+        var element = createSelect(propertyName, property.options, true);
+      } else if (property.type == "text") {
+        var element = document.createElement("input");
+        element.type = "text";
+        element.name = propertyName;
+        element.size = 42;
+      }
+      td.appendChild(element);
+      element.focus();
+      tr.appendChild(td);
+    }
+
+    // Add the add and remove buttons
+    td = document.createElement("td");
+    td.className = "actions";
+    var removeButton = document.createElement("input");
+    removeButton.type = "button";
+    removeButton.value = "-";
+    removeButton.onclick = function() { removeRow(removeButton, propertyName) };
+    td.appendChild(removeButton);
+    tr.appendChild(td);
+
+    if (alreadyPresent) {
+      existingTBody.appendChild(tr);
+    } else {
+      // Find the insertion point for the new row. We try to keep the filter rows
+      // in the same order as the options in the 'Add filter' drop-down, because
+      // that's the order they'll appear in when submitted.
+      var insertionPoint = getAncestorByTagName(select, "tbody");
+      outer: for (var i = select.selectedIndex + 1; i < select.options.length; i++) {
+        for (var j = 0; j < table.tBodies.length; j++) {
+          if (table.tBodies[j].rows[0].className == select.options[i].value) {
+            insertionPoint = table.tBodies[j];
+            break outer;
+          }
+        }
+      }
+      // Finally add the new row to the table
+      var tbody = document.createElement("tbody");
+      tbody.appendChild(tr);
+      insertionPoint.parentNode.insertBefore(tbody, insertionPoint);
+    }
+
+    // Disable the add filter in the drop-down list
+    if (property.type == "radio" || property.type == "checkbox") {
+      select.options[select.selectedIndex].disabled = true;
+    }
+    select.selectedIndex = 0;
+  }
+}
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/js/trac.js
@@ -0,0 +1,131 @@
+// Used for dynamically updating the height of a textarea
+function resizeTextArea(id, rows) {
+  var textarea = document.getElementById(id);
+  if (!textarea || (typeof(textarea.rows) == "undefined")) return;
+  textarea.rows = rows;
+}
+
+// A better way than for example hardcoding foo.onload
+function addEvent(element, type, func){
+  if (element.addEventListener) {
+    element.addEventListener(type, func, false);
+    return true;
+  } else if (element.attachEvent) {
+    return element.attachEvent("on" + type, func);
+  }
+  return false;
+}
+
+// Convenience function for the nearest ancestor element with a specific tag
+// name
+function getAncestorByTagName(e, tagName) {
+  tagName = tagName.toLowerCase();
+  do {
+    e = e.parentNode;
+  } while ((e.nodeType == 1) && (e.tagName.toLowerCase() != tagName));
+  return (e.nodeType == 1) ? e : null;
+}
+
+// Adapted from http://www.kryogenix.org/code/browser/searchhi/
+function searchHighlight() {
+  if (!document.createElement) return;
+
+  var div = document.getElementById("searchable");
+  if (!div) return;
+
+  function getSearchWords(url) {
+    if (url.indexOf('?') == -1) return [];
+    var queryString = url.substr(url.indexOf('?') + 1);
+    var params = queryString.split('&');
+    for (var p in params) {
+      var param = params[p].split('=');
+      if (param.length < 2) continue;
+      if (param[0] == 'q' || param[0] == 'p') { // q= for Google, p= for Yahoo
+        var query = unescape(param[1].replace(/\+/g, ' '));
+        if (query[0] == '!') query = query.slice(1);
+        words = query.split(/(".*?")|('.*?')|(\s+)/);
+        var words2 = new Array();
+        for (var w in words) {
+          words[w] = words[w].replace(/^\s+$/, '');
+          if (words[w] != '') {
+            words2.push(words[w].replace(/^['"]/, '').replace(/['"]$/, ''));
+          }
+        }
+        return words2;
+      }
+    }
+    return [];
+  }
+
+  function highlightWord(node, word, searchwordindex) {
+    // If this node is a text node and contains the search word, highlight it by
+    // surrounding it with a span element
+    if (node.nodeType == 3) { // Node.TEXT_NODE
+      var pos = node.nodeValue.toLowerCase().indexOf(word.toLowerCase());
+      if (pos >= 0 && !/^searchword\d$/.test(node.parentNode.className)) {
+        var span = document.createElement("span");
+        span.className = "searchword" + (searchwordindex % 5);
+        span.appendChild(document.createTextNode(
+          node.nodeValue.substr(pos, word.length)));
+        node.parentNode.insertBefore(span, node.parentNode.insertBefore(
+          document.createTextNode(node.nodeValue.substr(pos + word.length)),
+            node.nextSibling));
+        node.nodeValue = node.nodeValue.substr(0, pos);
+        return true;
+      }
+    } else if (!node.nodeName.match(/button|select|textarea/i)) {
+      // Recurse into child nodes
+      for (var i = 0; i < node.childNodes.length; i++) {
+        if (highlightWord(node.childNodes[i], word, searchwordindex)) i++;
+      }
+    }
+    return false;
+  }
+
+  var words = getSearchWords(document.URL);
+  if (!words.length) words = getSearchWords(document.referrer);
+  if (words.length) {
+    for (var w in words) {
+      if (words[w].length) highlightWord(div, words[w], w);
+    }
+  }
+}
+
+function enableControl(id, enabled) {
+  if (typeof(enabled) == "undefined") enabled = true;
+  var control = document.getElementById(id);
+  if (!control) return;
+  control.disabled = !enabled;
+  var label = getAncestorByTagName(control, "label");
+  if (label) {
+    label.className = enabled ? "enabled" : "disabled";
+  } else {
+    var labels = document.getElementsByTagName("label");
+    for (var i = 0; i < labels.length; i++) {
+      if (labels[i].htmlFor == id) {
+        labels[i].className = enabled ? "enabled" : "disabled";
+        break;
+      }
+    }
+  }
+}
+
+function addHeadingLinks(container) {
+  var base = document.location.pathname;
+  function addLinks(elems) {
+    for (var i = 0; i < elems.length; i++) {
+      var hn = elems[i];
+      if (hn.id) {
+        var link = document.createElement('a');
+        link.href = base + '#' + hn.id;
+        link.className = 'anchor';
+        link.title = "Link to this section";
+        link.appendChild(document.createTextNode(" \u00B6"));
+        hn.appendChild(link);
+      }
+    }
+  }
+  for (var lvl = 0; lvl <= 6; lvl++) {
+    addLinks(container.getElementsByTagName('h' + lvl));
+  }
+}
new file mode 100644
--- /dev/null
+++ b/examples/trac/htdocs/js/wikitoolbar.js
@@ -0,0 +1,88 @@
+function addWikiFormattingToolbar(textarea) {
+  if ((typeof(document["selection"]) == "undefined")
+   && (typeof(textarea["setSelectionRange"]) == "undefined")) {
+    return;
+  }
+  
+  var toolbar = document.createElement("div");
+  toolbar.className = "wikitoolbar";
+
+  function addButton(id, title, fn) {
+    var a = document.createElement("a");
+    a.href = "#";
+    a.id = id;
+    a.title = title;
+    a.onclick = function() { try { fn() } catch (e) { } return false };
+    a.tabIndex = 400;
+    toolbar.appendChild(a);
+  }
+
+  function encloseSelection(prefix, suffix) {
+    textarea.focus();
+    var start, end, sel, scrollPos, subst;
+    if (typeof(document["selection"]) != "undefined") {
+      sel = document.selection.createRange().text;
+    } else if (typeof(textarea["setSelectionRange"]) != "undefined") {
+      start = textarea.selectionStart;
+      end = textarea.selectionEnd;
+      scrollPos = textarea.scrollTop;
+      sel = textarea.value.substring(start, end);
+    }
+    if (sel.match(/ $/)) { // exclude ending space char, if any
+      sel = sel.substring(0, sel.length - 1);
+      suffix = suffix + " ";
+    }
+    subst = prefix + sel + suffix;
+    if (typeof(document["selection"]) != "undefined") {
+      var range = document.selection.createRange().text = subst;
+      textarea.caretPos -= suffix.length;
+    } else if (typeof(textarea["setSelectionRange"]) != "undefined") {
+      textarea.value = textarea.value.substring(0, start) + subst +
+                       textarea.value.substring(end);
+      if (sel) {
+        textarea.setSelectionRange(start + subst.length, start + subst.length);
+      } else {
+        textarea.setSelectionRange(start + prefix.length, start + prefix.length);
+      }
+      textarea.scrollTop = scrollPos;
+    }
+  }
+
+  addButton("strong", "Bold text: '''Example'''", function() {
+    encloseSelection("'''", "'''");
+  });
+  addButton("em", "Italic text: ''Example''", function() {
+    encloseSelection("''", "''");
+  });
+  addButton("heading", "Heading: == Example ==", function() {
+    encloseSelection("\n== ", " ==\n", "Heading");
+  });
+  addButton("link", "Link: [http://www.example.com/ Example]", function() {
+    encloseSelection("[", "]");
+  });
+  addButton("code", "Code block: {{{ example }}}", function() {
+    encloseSelection("\n{{{\n", "\n}}}\n");
+  });
+  addButton("hr", "Horizontal rule: ----", function() {
+    encloseSelection("\n----\n", "");
+  });
+  addButton("np", "New paragraph", function() {
+    encloseSelection("\n\n", "");
+  });
+  addButton("br", "Line break: [[BR]]", function() {
+    encloseSelection("[[BR]]\n", "");
+  });
+
+  textarea.parentNode.insertBefore(toolbar, textarea);
+}
+
+// Add the toolbar to all <textarea> elements on the page with the class
+// 'wikitext'.
+var re = /\bwikitext\b/;
+var textareas = document.getElementsByTagName("textarea");
+for (var i = 0; i < textareas.length; i++) {
+  var textarea = textareas[i];
+  if (textarea.className && re.test(textarea.className)) {
+    addWikiFormattingToolbar(textarea);
+  }
+}
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e48a1d1313b015379d0f875c724f21a83bcca0e8
GIT binary patch
literal 245
zc%17D@N?(olHy`uVBq!ia0vp^JRr=%3?!FCJ6-`&%*9TgAsieWw;%dH0CM>Qd_r9R
z|NsB?{oA#x)|Qo)sVJzN6*-y>lwvIj@(X4F%KewQpawFFv%n*=n1O-o0|+xZtudJa
z6jUs6jVKAuPb(=;EJ|hYO-wGz&rMCqOjK~oEJ`iUFUl@fFwrx3rKVmARBZ3*;uumf
zCpn>kQCyr|;iQs=2GbH5R@u2S4pX|7W)ulA1^G2~c`0}W3b@SF&|E2DU|_I&heh)i
fX=&*LK1>Wwzu7NZ)Kz~18pYu0>gTe~DWM4f-xo`!
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..cc973c44de3cc6afd11826cad270524164311b3c
GIT binary patch
literal 227
zc%17D@N?(olHy`uVBq!ia0vp^JRr=%3?!FCJ6-`&%*9TgAsieWw;%dH0CM>Qd_r9R
z|NsB<YwL%nmG_TyonGMlb-~J|Kq=OeAirP+pxl3%3u+*<I14-?iy0WWK7cTz(;AZ*
zKtaV4*NBqf{Irtt#G+IN-^Api{M^)(%tQsZ%%art{G#k)1rt4kS8D2|K*hSAE{-7;
zbCMGp7?KSV3`AI%7}=b~#Rc6{E%;qDn^|N7EA;vT<{7%|X1t=qknhNGZ*}iXQJ_W!
MPgg&ebxsLQ07Lgi&;S4c
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..7ece2987e22053aa21026f47b30b4f2eadb456ca
GIT binary patch
literal 228
zc%17D@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPGa4)6(a1=73s?%H>7@5L(@?%u!i
z^u_Zp-#`ER|7X>jmAj5HWB{cZOM?7@862M7NCR>>3p^r=85p=efH0%e8j~47LG}_)
zUsv`ktinuYW?iDeQa~YpPZ!6Kid)GE2^Rtq7?_uRQ{!-69N@&Lq@ZzwMNmM5g}buE
zq^!h(K~hJrS67cO;cBn0Kx~DiFJFKgyFkO4D+XnpzN;CG<|HWT=qWHT)O;7WznN%W
Q2{f9))78&qol`;+02sbV7ytkO
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d0605a3ee6b4c5946b5965ebb005b916aae914f9
GIT binary patch
literal 350
zc%17D@N?(olHy`uVBq!ia0vp^4M42G!2~2j9iA5fDb50q$YKTtZXpn6ymYtj4^WW3
z#M9T6{R)!+zoM01ZZ*h&zn(6RAr-gY&T<rLR^V~%T=d;ut2%I3LEFNHtJ}KwKS;e;
zC?oV_f168`x2B~}x?M{Hmlwmm>JWwl2N<4Jy0NM@WX=k3%qWv#*V16T(AKqGfgz(8
z=#DG~Esbo3SOJD(Y#ZKNYfWG*ag<<SyW4bzN0h<9fT526fp5hdo*UbEMW-+%^{ilM
zXk;u9QDi$1*doCYJK@^9ex);S9-fY}TvKZK{*S<``G)?pKYkC`K5_pnEB<#?KTBhO
zN@;ChkQy1_$hqKGeuf^WWqgoVT3Ow;<a4h-DeazOxcZIu-c6^Uzd6@`@m#cTRO_mk
sTal}@ZqzntNAAkbxn8|h=J_4&_2qSuM~x=90|SD=)78&qol`;+0B_-h>;M1&
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..5f7ed4ae26de6ac36b7c20b0977429801186691c
GIT binary patch
literal 309
zc$@(>0m}Y~P)<h;3K|Lk000e1NJLTq004jh001Ba0{{R3C=6{D0000mP)t-s9v~ee
zBq1g!B``8CGBYtVG%_|gH9tT;L`FkOOi52rPFGo0USM5kXl8D3ZFqWj&B*b*0002M
zNkl<ZNXOOFF%AJy5C-7?2;4yS1QJ(p0XGm42N2234Q6_cy#X$uP!bKDM#Q!Xg&GBB
z-<Hb1z*oF%=DqyMn|X_(bgjHTJgbd#s5tGC?J}8%S)SuE=lnpx<NpKhIR`iA;5^`9
z-{N5P^?-GogALon13She`w|zz!I+)GFyo+8=b#BvgAJ&$0U6uGKKn0B+{DddH5FGk
zcT<g8P$@rMG`6B3qEU>cJZ0%2n`6objMyvm*d}(_F|q+~-l^(R2@PdF00000NkvXX
Hu0mjf6_0rH
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3547a134ec27843f222dcf55327da701df3bd5ba
GIT binary patch
literal 3638
zc%1E4O^6&t6#llmrhEQ+x~HdmW_oIVcZ2LLB&P%d8*<Gl*W{EOLIi~n0!nfig&Y&X
zKu&QHApwbDk<Bg$x@vUSbxGWKkX?eT#!XlyYBGvHBMfO@Rrk#FtmEFq%Y5*<>eW~M
z-mCYTR}G9ofn`DBR1A2I#(F)_-;M*X(b^=<#TKzVhPdPoj$tT#-^bCTxAD%+Z%{7Z
z!1{U%7cP8>ef#Ec`t%AG7PfHp>U|tO`~e<4`W-7PKVoun28Ry4hs&4OaQ^%%=I0;c
z)bmR?etZ$vuWw*u<5%q6JBuSnKE%aKUt#s$PiQo5Vs>^D%gc|42QtD4Bm57ryg@y~
z<FUS$fWvu4H?=@jcosV6oE4<%a-O0!&WpCxRhCI0naOy_N~<MDtQ!?roYO3qgI7oa
zd1#zFv^^H*+(b5`<}+NSoiuPeona<iR~por4wV9cmJA#?hH$FGE(Ls^9*&vlhbtbX
z!4nnAgL*O?J3h%2@S6^7l6)d02P`*$*WUtOrJngQ>6@W^Ujv@feqTs4=(BZ_gXs6!
zSJ?0@i#~ho*lo<re2Y@)hU~FxYfrI%|2)o|S-~6A_wnS(pIBP@fqL!xvd5l0c^8+c
ze~Mmv?%WsHvu75ot3S)0ytVZQu3g)}l`HGA--`Zv;>056<{nbtUBC-3F3Vp1(#xOY
z&fUk@z5899J$nbYZheR8>92<eIl>4d{HJhY)t%@G+q2Cb_zLqpFJ1pv-t+{FQo^a$
z-R)%3MbY*G7=-Zw)*aXFdT1bo6QMPoF-DEG#QJ<T!dqgU8mMOU`9kE}fmatvy~_?9
zG#~KPnnT`>yxik?Hw9kWg-iEFkFN^1^4=<1B9J7KG#V3;_>5XO<uQ|TqY5+Ko54*L
zV`R+L`X0vx->A!ZPS@#;s?zYRVwbPTfOpAxjnV(MX|V9jj!C-;GUC9eX=!vLN{|=T
zV0tk~l#%b6n+Wnofr}ckO5mW<3r7j^9t#uDpiC-8IoadWlDBCgD6ufyPY+$|a<$Sc
zP+SBY+29yq#krg?g-RVQAYx&`)n4sQBl?MHiHbZ-O1^K<HLpefBamuz&!V%y{2;Q-
z>PBLF(SwCB?f%#3INMqJxO2T}weU|m8fe29@_rw?Jfpr7Up3-8+DB}g?*Br!{{taY
zB=jp#N3&yLs$C4E6-Tp`9`jperPeyY%yu(vKDgJ>KUF*WXX-_LQDu5t(ag4{kf<NE
g(`rpfr%ZA{ooYYReJTE%{$D=%cvcIA=6=)v8%%NhaR2}S
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..3e040983c1ec364c2338e688a4c4f89712487023
GIT binary patch
literal 2161
zc$@)o2#)uOP)<h;3K|Lk000e1NJLTq008U&002n{0{{R33EVn+0000;P)t-s0096H
z5e^s`7at%VB_|{<FfBGWHNpS@K0iH0M?_9gO;=b}%`!4-YiZY6S%`{<;e&#ytElOr
zq3yS~%gxI2&(G=Q<Ng2q*oPOc000N%Nkl<Zc-rloi;|-%5J1HTib#-%&Hw-D?Z#+O
zAtp0gb!T>{txQ%CJ@&gbnfr%E_6Pe1`v?1rV8`(vg6-=2KLdNO>w5fGU~dT9{Y$XF
zNs&*5wc7k#*nwdCcR^d0W$Fie<NiGl1xDtXA3p^)3U%+^5v<JS@k3z8x`VGS^5bC9
z8Qo%LdmrHU!FDmnd#A1S4SpkR7YjYK9e$r0Z(;BCJi2egS}l3A?6pQIDR|Q=WgzL|
zwUP}NQfULHJE)z}*3Xg7EVAfucK+6SIN`tN{s_~o=T<s$Z&Z0W=R2Wi7iES3wo3-6
zBiIJ#=*Rb*DA;~>zDc|c-%V-GPAW!J<!)GI6kE{_yBy;PmOoh$*2Jf;)F1f4gfn1=
zjmF;Uy=c2#d^&<H1Hjxt0ydLRv9j2Hb1v*SbM-OF*2^6D3^sRF0PC6*?QE0@*~7W8
z-NH4-V8>bb_qSjd5whUw^FXB&I0@F`TyGq>qgxDin1fOG8ElrXU?+)JtuodcEw`ez
z>`2Q`ODfi)jYziN!wMWvo<d9`Zk!Z)imcR6owQFKXMVMCI#b$$0}RT!J{L9w>(@s7
zv{{IU+*nev;bc|Y@{06y?P`F9dTdt?_@q=~4?9L&Z)DRw2u&ZrHa2+yB6PSx&|&tv
zbaB`l=qmQiE*jtqun8qZ1vR<~X2J0>7r=Hqqn+@fi`GB~u=|7nbX&50-VnHIacLp2
zf}IR=+?wo=sDTb(xARRE^|&H*PxeySTST<CwHCuVG`$Pkt|#j}5okGj>Sye_xGl4f
zG)p|%u-`v|U0ENr$*h70$COvWj*}VQ>vZ%PHn#)53JXP+hJ~ayoCkY<NUo2Cnm})F
zTX*#Z*j;MOSQM6D2s`2Qkpgz;max<91JS3jVP?!E<wrp>9IEkUHaje`8oC#a7^uGw
zTkPXe2%BH;jmM|Yfm;G+W7A1BmM>vVq&^K{`L#KP_r_u227MQeE^F9LyFx{Bf!KAc
zb=JSpY#!{-7+e*4#v2L}h6lnpMEl}lj<wmZBM>pzm<Ff-Hmn1xE930_s2e`kNrR55
zXAG9_LrYNgpa%|9$QCYx9aG(K821ipn^c4np<)&6kz{8r4V!Cx+sAm;c=p_S_Tibj
zl{F52Yj@u!l-pafuGE?DqQqbWmIiiNGAZt+Iw)JEl@u&6k27HF$ditZYaZ%UZoIul
zYDvUkSG75pqS*B@^y?1QIk02BC%J9oYM&5n=w%i^6^$Y+dokEr)30l#_WOik+^5R^
zp<WkxYHi<yHLqYZx9dWk9C!DybH=dlw><<tCe%s22OAL_D>kaCqOk2nuyw4!UEEY+
z@U4&%RnN9=FRa?8h|xy^#Z|E5uDALIu6_%4J5NHO+IDH$u!N4E1AE`M+v@%3`WH^o
zwo4w*Fl*$?*!UJ$<}pclc9|F?|I6z)lb@Cbt_VGgWTB3Y?&XU@^!!+Bb6BzXc}LU0
z;4GYG>}jb~7HL7jzTH@-W9$zaSt|4G<FNH_fDLH-NdN7Z!Md>vOcuHXYxg@~XU**+
zK{nZt`FL3PpTJU^VtR`tWw5clm<Zkv`GNwG;SB#MmOD)iV&VHyrm4Jtwyczaqt^S>
z6!+@#+pw|TTI8|F9yXt^ES|564QxR$n&PUa9Fh6HMPs!GGu-Lm*|XWMV<}4!`G=fF
zr(hF;?fOKNMObSwZdxu4=0AjDtZ@d6!#|iKVNO6_a2COWuWv}WPBl11cnJQ4#R+t*
z2Q^f>gAcpLcLt2WTTvCxuL#Dk>mT~!cRPJGpaCckp=-e_!;5O^#8KF_cj|8aeS}xG
zhOnwo%-9NHOO=BwWyM`pNl>an@o6wzN@sHA5xJ&<pH@^TM6G>GT#H9R&rzdY&XucZ
zr3|1{sI+BEutcK@XC-b?3h!|)aiY;^2{u=`a!OXlWvwh*Sve?h1w3h~K{P4Y4+?-8
zmM&U3sh!17V};X=<K7k-0G<TRFn4q-ybMabKyT+Q9@^M)s&=gNk}Gv#T%nQ5DZFq%
zfpcrxSq-%&g)iyGRMs-*kH!z2hNmwv({#8J{R3P9Z&Bs&V6EICZ1h2U6jl*0o=umc
z)c8M?4rRr<%`Cy@LS#_DWV1-Ctv|D{95|2x8i6A9?;+$QKQwElY1Is5C32(m6ISci
zqBAoRb*Uho2Q4yMS!v4zFYy?>#g183ib5OV-M}&Io5|q{R-wi(u(++9U@dOWXIR_|
zzR0Oi&y{0dp<S!wq{eQ6l?00h=t5X4IjRGmKs;bu4Kr*(_O`&%kw>t%PsAF+;w72J
zS2A9W4JmGJ=_HaMXEk;--M^z+f<Y_JrY(?}5SG+flN!^+NsYak<B}596Zi24Y!0X=
zIIY#rw=HK_BfQGIhV@TDy7zzMvcMW-DWyUIiq^v#bTZa)38tVMkxPQLLZZo8R!NfS
zbY*16Ra=IzP{_hNr~-u#mC&xCUNYV>L3h#H_5>@X${c#fN;_3)v%umC3MN>~O#0U!
zAN}NzLX7p@VvIlFYd1i2%9bq9t4u^lp}<<7L0R*t;e243V=2Y1N7I?2$0O`y?y;g@
z+OO!QPg?pBp!`A(6@EI;ywM*C)0I(Jy6h*V?KypI^GT{y*e_&I-yTqu=e)87)!WZ5
nZBZWG-@hLV|H1yj{;csI%a+zc7QYGr00000NkvXXu0mjft^FK7
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..38c038d64fa3c1d572f369d744da36768a1ca3e9
GIT binary patch
literal 689
zc$@*T0#5yjP)<h;3K|Lk000e1NJLTq003(M0015c0{{R39F7o70000yP)t-s0096H
z5)T_48YwC$F*7heKRvA_Bwb%!#$;rNhliJ$meh=jtE;Qrq^0Dzx6#VS&d$#7<>dYU
z{Qv*|^a>K50006oNkl<Zc-pm=i;{yN3`Ow;G*q&Y|NqxTAc&}FXIf^e7+a4wxrB=4
za?G@RcZlm-Ql10}6#Rr`>ktk?2=xQjEDuQp@l_V-Mw!0MvQlSV((FlUT3Lfq3jN0y
zj$rB_+!(rkDk|wCeK#t=wk~FQR0UHak61VgN9b~RiaCLv1JJIQwI+B%udKCXmabSK
zpkND&q52-RSw=<!29%ur#GJ53e~53dNC=-TP%MPm{&sicjey83Q>-(>=nX`qDLv3r
zmQ)oCfOr!9nmxoe%dni7L$6Tl6CX}V)3Uus)DzYB^u&jerFjwxc*PociL1>X*dIL6
z@O;uk8&hAf;7~`;I(p)Q<@>E>VRJy*l*wyWLUltB2aj2vL7I~{YnxJO8mu6s14^-|
z3m>qwOpn)Fq-KqKmddj+uRJk5sh3@5yR(B%w%lwT611`MEGbfzL)$(~D(q}1vG|7+
zD>DvQ>FNPfV?v6a?0mx<Zu&-+ksx;06-Rd4#wuG>h?Rk6?<HQr$@{d>Tx^+mWG&o6
zI*t|1817klDf{VubLLDP*Vl;1G2*q|!)*>%mX<tB^*;artHS#RbrOQA9bWVe!>yN>
zS{Ux{{rYUY|D0>?U!h9ojO9_zx$u;Da{O`Iad+`<v%>1K1<LMN84yn{^U8bq;+B;+
zWvPuR{*e~CYSxLa$1GWPi%Qq)T31f&C#-cNIdqwV^>X{iyE3t#)GK)Qq@R7?hrb*D
X?NO$K)J1bN00000NkvXXu0mjfXa+<g
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8a72b09bb9f09b273ee7b5553a50c7bfacf67a14
GIT binary patch
literal 233
zc%17D@N?(olHy`uVBq!ia0vp^JRr=%3?!FCJ6-`&%*9TgAsieWw;%dH0CM>Qd_r9R
z|NsB_^XJ>QZ_k@Iud=eTG-%O%pcHFKkY6wZQ0~9X1vQXaoCO|{#S9EwA3&JVX^qJY
zprB%jYeY$Kep*R+Vo@rCZ(?#$er{??W}<>yW>IQ+eo=O@f{C8ND>d~}pkiZB7srr_
zImroyg@uU;2?>HCB}EKDhmQ4WCCqD{z})ft(IbXOj6W6TZB*2go5CdZ>=6S)aW<!u
To?~Y{P&<RCtDnm{r-UW|-O)~L
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..fcb4a4406386dabe4f82ecbb07088d0218beb6c9
GIT binary patch
literal 452
zc%17D@N?(olHy`uVBq!ia0vp^DnQJ~!3-n|Ui$q4QU?NjLR_y#uzb97;9)Gw|8F1u
z7l}St-t_(DgQsyUUs71#p4|8$nf2F~_xI-&{a;=D{lSF`MaIwfF8u%a;(SNu|Ie;I
zZE^nJYV!Zq!E>Q3XVaDczkT?8^TZdco312^e8>@bQm%J5PvyhGg+Hzz{9mK;tlIeV
zp@qMu1%FuAq{zqkb5qm*i<|!UyZqnMRBO!ie^&UR02X^G#{Val{0D+G1|q@J)z`iP
zDb50q$YLPv0mg18v+aP4Rh}-6Ar-f#UU<pZWFWwjuwZKRf#3VyhlMYD|Gyyb=Teqt
zzfGRzKaDc4NlsZCY0ZCm)^GV0+={(U{#gkrKAa!S)|tN*I4SvwbFtDC29JZKSsC*c
zoV^yhHT8;}Ju!!2I@97n1)<~m9*qAk8NAAA%ru_llrkgyMMKA|i@ce9Ji4EpinN~S
zZwZrEf50F6aJ|l>y-sG&f2`G1c=xTNLdWP&*U4kQ|33Pgo~@_9p*46WpL13o$V;BC
KelF{r5}E)eJH@>K
new file mode 100644
--- /dev/null
+++ b/examples/trac/scripts/rpm-install.sh
@@ -0,0 +1,16 @@
+#! /bin/sh
+#
+# this file is *inserted* into the install section of the generated
+# spec file
+#
+
+# this is, what dist.py normally does
+./setup.py install --root=${RPM_BUILD_ROOT} --record="INSTALLED_FILES"
+
+# catch compressed man pages
+sed -i -e 's@\(.\+/man/man[[:digit:]]/.\+\.[[:digit:]]\)$@\1*@' "INSTALLED_FILES"
+
+# catch any compiled python files (.pyc, .pyo), but don't list them twice
+sed -i -e 's@\(.\+\)\.py$@\1.py*@' \
+       -e '/.\+\.pyc$/d' \
+       "INSTALLED_FILES"
new file mode 100755
--- /dev/null
+++ b/examples/trac/scripts/trac-admin
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+# -*- coding: iso8859-1 -*-
+__author__ = 'Daniel Lundin <daniel@edgewall.com>, Jonas Borgström <jonas@edgewall.com>'
+__copyright__ = 'Copyright (c) 2005 Edgewall Software'
+__license__ = """
+ Copyright (C) 2003, 2004, 2005 Edgewall Software
+ Copyright (C) 2003, 2004 Jonas Borgström <jonas@edgewall.com>
+ Copyright (C) 2003, 2004 Daniel Lundin <daniel@edgewall.com>
+ All rights reserved.
+
+ This software is licensed as described in the file COPYING, which
+ you should have received as part of this distribution. The terms
+ are also available at http://trac.edgewall.com/license.html.
+
+ This software consists of voluntary contributions made by many
+ individuals. For the exact contribution history, see the revision
+ history and logs, available at http://projects.edgewall.com/trac/."""
+
+import sys
+
+from trac.scripts.admin import run
+sys.exit(run(sys.argv[1:]))
new file mode 100644
--- /dev/null
+++ b/examples/trac/scripts/trac-admin.1
@@ -0,0 +1,20 @@
+.\" You can view this file with:
+.\" nroff -man [filename]
+.\"
+.TH trac-admin 1
+.SH NAME
+trac-admin \- Trac administration tool
+.SH SYNOPSIS
+.TP
+\fBtrac-admin\fP \fI</path/to/projenv>\fP [\fIcommand\fP [\fIsubcommand\fP] [\fIoption ...\fP]]
+.SH OVERVIEW
+Trac is a minimalistic web-based software project management and bug/issue
+tracking system. It provides an interface to the Subversion revision control
+systems, an integrated wiki, flexible issue tracking and convenient report
+facilities.
+
+Documentation for Trac, including detailed usage explanations and
+tutorials can be be found in the Trac wiki at
+http://trac.edgewall.com/.
+
+Run `trac-admin help' to access the built-in tool documentation.
new file mode 100755
--- /dev/null
+++ b/examples/trac/scripts/trac-postinstall.py
@@ -0,0 +1,51 @@
+# Post installation script for the Windows installer
+# This script is needed to create the trac/siteconfig.py file containing various
+# global default directories
+
+import os.path
+import sys
+from distutils import sysconfig
+import trac
+
+def install():
+    print 'Setting up default directories...'
+    site_packages = os.path.join(sysconfig.get_config_var('BINLIBDEST'),
+                                 'site-packages')
+    prefix = sysconfig.get_config_var('prefix')
+
+    conf_dir = os.path.join(prefix, 'share', 'trac', 'conf')
+    templates_dir = os.path.join(prefix, 'share', 'trac', 'templates')
+    htdocs_dir = os.path.join(prefix, 'share', 'trac', 'htdocs')
+    wiki_dir = os.path.join(prefix, 'share', 'trac', 'wiki-default')
+    macros_dir = os.path.join(prefix, 'share', 'trac', 'wiki-macros')
+
+    siteconfig = os.path.join(site_packages, 'trac', 'siteconfig.py')
+    fd = open(siteconfig, 'w')
+    fd.write("""
+# PLEASE DO NOT EDIT THIS FILE!
+# This file was autogenerated when installing Trac %(version)s.
+#
+__default_conf_dir__ = %(conf)r
+__default_templates_dir__ = %(templates)r
+__default_htdocs_dir__ = %(htdocs)r
+__default_wiki_dir__ = %(wiki)r
+__default_macros_dir__ = %(macros)r
+
+""" % {'version': trac.__version__, 'conf': conf_dir,
+       'templates': templates_dir, 'htdocs': htdocs_dir,
+       'wiki': wiki_dir, 'macros': macros_dir})
+    fd.close()
+
+    file_created(siteconfig)
+    print 'Done.'
+
+def remove():
+    pass
+
+
+if __name__ == '__main__':
+    mode = sys.argv[1]
+    if mode == '-install':
+        install()
+    elif mode == '-remove':
+        remove()
new file mode 100755
--- /dev/null
+++ b/examples/trac/scripts/tracd
@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+
+if __name__ == '__main__':
+    from trac.web.standalone import main
+    main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/scripts/tracd.1
@@ -0,0 +1,24 @@
+.\" You can view this file with:
+.\" nroff -man [filename]
+.\"
+.TH tracd 1
+.SH NAME
+tracd \- Stand alone Trac HTTP server
+.SH SYNOPSIS
+.TP
+\fBtracd\fP \fI[options]\fP <\fIprojenv\fP> [\fIprojenv\fP] ...
+.SH OVERVIEW
+
+\fBtracd\fP is a simple stand alone Trac HTTP server. It can be used as an
+alternative to using \fBtrac.cgi\fP with apache.
+
+Trac is a minimalistic web-based software project management and bug/issue
+tracking system. It provides an interface to the Subversion revision control
+systems, an integrated wiki, flexible issue tracking and convenient report
+facilities.
+
+Documentation for tracd, Trac in general, including detailed usage explanations
+and tutorials can be be found in the Trac wiki at
+http://trac.edgewall.com/.
+
+Run `tracd -h' to access the built-in tool documentation.
new file mode 100755
--- /dev/null
+++ b/examples/trac/scripts/tracdb2env
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+import os
+import sys
+import sqlite
+import ConfigParser
+from trac.env import Environment
+
+additional_config = \
+(('notification', 'smtp_enabled', 'false'),
+ ('notification', 'smtp_server', 'localhost'),
+ ('notification', 'smtp_replyto', 'trac@localhost'),
+ ('logging', 'log_type', 'none'),
+ ('logging', 'log_file', 'trac.log'),
+ ('logging', 'log_level', 'DEBUG'),
+ ('attachment', 'max_size', '262144'),
+ ('trac', 'default_charset', 'iso-8859-15'),
+ ('trac', 'database', 'sqlite:db/trac.db'))
+
+def db2env(db_path, env_path):
+    env = Environment(env_path, create=1)
+    # Open the databases
+    old_cnx = sqlite.connect(db_path)
+    new_cnx = env.get_db_cnx()
+    old_cursor = old_cnx.cursor()
+    new_cursor = new_cnx.cursor()
+    convert_config(env, old_cursor)
+    convert_db(old_cursor, new_cursor)
+    new_cursor.execute("INSERT INTO system VALUES('database_version', '7')")
+    new_cnx.commit()
+
+def convert_config(env, old_cursor):
+    old_cursor.execute('SELECT section, name, value FROM config')
+    while 1:
+        row = old_cursor.fetchone()
+        if not row:
+            break
+        row = [row[0], row[1], row[2]]
+        if row[0] == 'general':
+            row[0] = 'trac'
+        if row[1] == 'database_version':
+            continue
+        env.set_config(row[0], row[1], row[2])
+    for v in additional_config:
+        env.set_config(*v)
+    env.save_config()
+
+def to_utf8(row):
+    x = []
+    for v in row:
+        if type(v) == type(''):
+            try:
+                u = unicode(v, 'utf-8')
+                x.append(v)
+            except UnicodeError:
+                u = unicode(v, 'iso-8859-15')
+                x.append(u.encode('utf-8'))
+        else:
+            x.append(v)
+    return x
+
+def copy_tuples(table, from_cursor, to_cursor, fields='*'):
+    from_cursor.execute('SELECT %s FROM %s' % (fields, table))
+    while 1:
+        row = from_cursor.fetchone()
+        if not row:
+            break
+        row = to_utf8(row)
+        if fields == '*':
+            to_cursor.execute('INSERT INTO %s VALUES(%s)' \
+                              % (table, ', '.join(['%s'] * len(row))), *row)
+        else:
+            to_cursor.execute('INSERT INTO %s (%s) VALUES(%s)' \
+                              % (table, fields,
+                                 ', '.join(['%s'] * len(row))), *row)
+
+def convert_db(old_cursor, new_cursor):
+    copy_tuples('revision', old_cursor, new_cursor)
+    copy_tuples('node_change', old_cursor, new_cursor)
+    copy_tuples('auth_cookie', old_cursor, new_cursor)
+    copy_tuples('enum', old_cursor, new_cursor)
+    copy_tuples('ticket_change', old_cursor, new_cursor)
+    copy_tuples('permission', old_cursor, new_cursor)
+    copy_tuples('component', old_cursor, new_cursor)
+    copy_tuples('milestone', old_cursor, new_cursor, "name, time")
+    new_cursor.execute("UPDATE milestone SET descr=''")
+    copy_tuples('version', old_cursor, new_cursor)
+    copy_tuples('report', old_cursor, new_cursor,
+                'id,author,title,sql')
+    copy_tuples('ticket', old_cursor, new_cursor,
+                'id,time,changetime,component,severity,priority,'
+                'owner,reporter,cc,url,version,milestone,status,'
+                'resolution,summary,description')
+    copy_tuples('wiki', old_cursor, new_cursor,
+                'name,version,time,author,ipnr,text')
+
+if __name__ == '__main__':
+    if len(sys.argv) != 3:
+        print >> sys.stderr, 'Usage: %s <db-file> <env-dir>\n' % sys.argv[0]
+        print >> sys.stderr, \
+              'Creates a new Trac environment and initializes it with ' \
+              'information\nfrom an existing pre 0.7 trac database.'
+        print >> sys.stderr
+        sys.exit(1)
+    db2env(sys.argv[1], sys.argv[2])
+    print >> sys.stderr, 'Environment successfully created.'
new file mode 100644
--- /dev/null
+++ b/examples/trac/scripts/tracdb2env.1
@@ -0,0 +1,26 @@
+.\" You can view this file with:
+.\" nroff -man [filename]
+.\"
+.TH tracdb2env 1
+.SH NAME
+tracdb2env \- Tool to convert old Trac databases to Trac environments.
+.Sh SYNOPSIS
+.TP
+\fBtracdb2env\fP <\fIdb-file\fP> <\fIenv-dir\fP>
+.SH OVERVIEW
+
+Starting with version 0.7 Trac stores project specific information in project
+environment directories instead of in database files. \fBtracdb2env\fP can extract
+information from an old database file and insert that into a newly created
+project environment directory.
+
+Trac is a minimalistic web-based software project management and bug/issue
+tracking system. It provides an interface to the Subversion revision control
+systems, an integrated wiki, flexible issue tracking and convenient report
+facilities.
+
+Documentation for Trac in general, including detailed usage explanations
+and tutorials can be be found in the Trac wiki at
+http://trac.edgewall.com/.
+
+Run `tracdb2env -h' to access the built-in tool documentation.
new file mode 100755
--- /dev/null
+++ b/examples/trac/setup.py
@@ -0,0 +1,249 @@
+#!/usr/bin/env python
+
+import os
+import os.path
+import sys
+import string
+from glob import glob
+from distutils.core import setup
+from distutils.command.install import install
+from distutils.command.install_data import install_data
+from distutils.command.install_scripts import install_scripts
+from stat import ST_MODE, S_ISDIR
+
+import trac
+
+PACKAGE = 'Trac'
+VERSION = str(trac.__version__)
+URL = trac.__url__
+LICENSE = trac.__license__
+
+if sys.version_info < (2, 3):
+    print >>sys.stderr, 'You need at least Python 2.3 for %s %s' \
+                        % (PACKAGE, VERSION)
+    sys.exit(3)
+
+def _p(unix_path):
+     return os.path.normpath(unix_path)
+
+class my_install (install):
+     def run (self):
+         self.siteconfig()
+
+     def siteconfig(self):
+         path = self.prefix or self.home
+         path = os.path.expanduser(path)
+         conf_dir = os.path.join(path, 'share', 'trac', 'conf')
+         templates_dir = os.path.join(path, 'share', 'trac', 'templates')
+         htdocs_dir = os.path.join(path, 'share', 'trac', 'htdocs')
+         wiki_dir = os.path.join(path, 'share', 'trac', 'wiki-default')
+         macros_dir = os.path.join(path, 'share', 'trac', 'wiki-macros')
+         plugins_dir = os.path.join(path, 'share', 'trac', 'plugins')
+         f = open(_p('trac/siteconfig.py'), 'w')
+         f.write("""
+# PLEASE DO NOT EDIT THIS FILE!
+# This file was autogenerated when installing %(trac)s %(ver)s.
+#
+__default_conf_dir__ = %(conf)r
+__default_templates_dir__ = %(templates)r
+__default_htdocs_dir__ = %(htdocs)r
+__default_wiki_dir__ = %(wiki)r
+__default_macros_dir__ = %(macros)r
+__default_plugins_dir__ = %(plugins)r
+
+""" % {'trac': PACKAGE, 'ver': VERSION, 'conf': _p(conf_dir),
+       'templates': _p(templates_dir), 'htdocs': _p(htdocs_dir),
+       'wiki': _p(wiki_dir), 'macros': _p(macros_dir),
+       'plugins': _p(plugins_dir)})
+         f.close()
+
+         # Run actual install
+         install.run(self)
+         print
+         print "Thank you for choosing Trac %s. Enjoy your stay!" % VERSION
+         print
+
+class my_install_scripts (install_scripts):
+    def initialize_options (self):
+        install_scripts.initialize_options(self)
+        self.install_data = None
+        
+    def finalize_options (self):
+        install_scripts.finalize_options(self)
+        self.set_undefined_options('install',
+                                   ('install_data', 'install_data'))
+          
+    def run (self):
+        if not self.skip_build:
+            self.run_command('build_scripts')
+
+        self.outfiles = []
+
+        self.mkpath(os.path.normpath(self.install_dir))
+        ofile, copied = self.copy_file(os.path.join(self.build_dir,
+                                                     'trac-admin'),
+                                        self.install_dir)
+        if copied:
+            self.outfiles.append(ofile)
+        ofile, copied = self.copy_file(os.path.join(self.build_dir,
+                                                     'tracd'),
+                                        self.install_dir)
+        if copied:
+            self.outfiles.append(ofile)
+        ofile, copied = self.copy_file(os.path.join(self.build_dir,
+                                                     'tracdb2env'),
+                                        self.install_dir)
+        if copied:
+            self.outfiles.append(ofile)
+            
+        cgi_dir = os.path.join(self.install_data, 'share', 'trac', 'cgi-bin')
+        if not os.path.exists(cgi_dir):
+            os.makedirs(cgi_dir)
+            
+        ofile, copied = self.copy_file(os.path.join(self.build_dir,
+                                                    'trac.cgi'), cgi_dir)
+        if copied:
+            self.outfiles.append(ofile)
+
+        ofile, copied = self.copy_file(os.path.join(self.build_dir,
+                                                    'trac.fcgi'), cgi_dir)
+        if copied:
+            self.outfiles.append(ofile)
+         
+        for path in ('plugins', 'conf'):
+            full_path = os.path.join(self.install_data, 'share', 'trac', path)
+            if not os.path.exists(full_path):
+                os.makedirs(full_path)
+            
+        if os.name == 'posix':
+            # Set the executable bits (owner, group, and world) on
+            # all the scripts we just installed.
+            for file in self.get_outputs():
+                if not self.dry_run:
+                    mode = ((os.stat(file)[ST_MODE]) | 0555) & 07777
+                    os.chmod(file, mode)
+        elif os.name == 'nt':
+            # Install post-install script on windows
+            ofile, copied = self.copy_file(os.path.join(self.build_dir,
+                                                        'trac-postinstall.py'),
+                                            self.install_dir)
+            if copied:
+                self.outfiles.append(ofile)
+
+
+class my_install_data (install_data):
+    def run (self):
+        install_data.run(self)
+
+        if os.name == 'posix' and not self.dry_run:
+            # Make the data files we just installed world-readable,
+            # and the directories world-executable as well.
+            for path in self.get_outputs():
+                mode = os.stat(path)[ST_MODE]
+                if S_ISDIR(mode):
+                    mode |= 011
+                mode |= 044
+                os.chmod(path, mode)
+
+# Our custom bdist_wininst
+import distutils.command.bdist_wininst
+from distutils.command.bdist_wininst import bdist_wininst
+class my_bdist_wininst(bdist_wininst):
+    def initialize_options(self):
+        bdist_wininst.initialize_options(self)
+        self.title = 'Trac %s' % VERSION
+        self.bitmap = 'setup_wininst.bmp'
+        self.install_script = 'trac-postinstall.py'
+distutils.command.bdist_wininst.bdist_wininst = my_bdist_wininst
+
+
+# parameters for various rpm distributions
+rpm_distros = {
+    'suse_options': { 'version_suffix': 'SuSE',
+                      'requires': """python >= 2.3
+                        subversion >= 1.0.0
+                        pysqlite >= 0.4.3
+                        clearsilver >= 0.9.3
+                        httpd""" },
+
+    'fedora_options': { 'version_suffix': 'fc'}
+    }
+
+
+# Our custom bdist_rpm
+import distutils.command.bdist_rpm
+from distutils.command.bdist_rpm import bdist_rpm
+class generic_bdist_rpm(bdist_rpm):
+
+    def __init__(self, dist, distro):
+        self.distro = distro
+        bdist_rpm.__init__(self, dist)
+
+    def initialize_options(self):
+        bdist_rpm.initialize_options(self)
+        self.title = "Trac %s" % VERSION
+        self.packager = "Edgewall Software <info@edgewall.com>"
+        for x in rpm_distros[self.distro].keys():
+            setattr(self, x, rpm_distros[self.distro][x])
+        self.install_script = "scripts/rpm-install.sh"
+
+    def run(self):
+        bdist_rpm.run(self)
+        if hasattr(self, 'version_suffix'):
+            prefix = os.path.join(self.dist_dir, string.lower(PACKAGE)+'-'+VERSION+'-1')
+            os.rename(prefix+'.noarch.rpm', prefix+self.version_suffix+'.noarch.rpm')
+            os.rename(prefix+'.src.rpm', prefix+self.version_suffix+'.src.rpm')
+
+class proxy_bdist_rpm(bdist_rpm):
+
+    def __init__(self, dist):
+        bdist_rpm.__init__(self, dist)
+        self.dist = dist
+
+    def initialize_options(self):
+        bdist_rpm.initialize_options(self)
+
+    def run(self):
+        for distro in rpm_distros.keys():
+            r = generic_bdist_rpm(self.dist, distro)
+            r.initialize_options()
+            self.dist._set_command_options(r, self.dist.command_options['bdist_rpm'])
+            r.finalize_options()
+            r.run()
+
+distutils.command.bdist_rpm.bdist_rpm = proxy_bdist_rpm
+
+setup(name="trac",
+      description="Integrated scm, wiki, issue tracker and project environment",
+      long_description=\
+"""
+Trac is a minimalistic web-based software project management and bug/issue
+tracking system. It provides an interface to the Subversion revision control
+systems, an integrated wiki, flexible issue tracking and convenient report
+facilities.
+""",
+      version=VERSION,
+      author="Edgewall Software",
+      author_email="info@edgewall.com",
+      license=LICENSE,
+      url=URL,
+      packages=['trac', 'trac.db', 'trac.mimeview', 'trac.scripts',
+                'trac.ticket', 'trac.upgrades', 'trac.util', 'trac.web',
+                'trac.versioncontrol', 'trac.versioncontrol.web_ui', 
+                'trac.wiki'],
+      data_files=[(_p('share/trac/templates'), glob('templates/*')),
+                  (_p('share/trac/htdocs'), glob(_p('htdocs/*.*')) + [_p('htdocs/README')]),
+                  (_p('share/trac/htdocs/css'), glob(_p('htdocs/css/*'))),
+                  (_p('share/trac/htdocs/js'), glob(_p('htdocs/js/*'))),
+                  (_p('share/man/man1'), glob(_p('scripts/*.1'))),
+                  (_p('share/trac/wiki-default'), glob(_p('wiki-default/[A-Z]*'))),
+                  (_p('share/trac/wiki-macros'), glob(_p('wiki-macros/*.py')))],
+      scripts=[_p('scripts/trac-admin'),
+               _p('scripts/trac-postinstall.py'),
+               _p('scripts/tracd'),
+               _p('scripts/tracdb2env'),
+               _p('cgi-bin/trac.cgi'),
+               _p('cgi-bin/trac.fcgi')],
+      cmdclass = {'install': my_install,
+                  'install_scripts': my_install_scripts,
+                  'install_data': my_install_data})
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e309c12d33789e0e5621236261bf02f70ad25afe
GIT binary patch
literal 39982
zc%1E>O^oE)Re)VD)u~jySM{D#<2RM6vD?q>v1{yd+g)vsJ$CzE$RY_+K!5~n60ryo
zLLgW`B7Ouy5V7D-K_G-e1VSPpktqBN1%X%~fe;czEC5AF0VEb6gh1J_K)^Zo$7NU9
zRWm(P(=F)J-Tt{h=YIFxbI-M_X6$!=<@qNSCXaiHvV{H5!QTlS_mp?A^#A;XqP+7}
z{`X}H|AS2B?W1=nue@R@Cw*U;sBb9u)Cpv~TX}Q#rZRp#RP<9ru^n4EdF5lu8<X!)
zyuep3E-#?WuPKiEAtlW-<vjebLcW|TuP9$o-t)fqC|Be|%3E)JMtT2Ny<d6nd%s2b
z>aYD8<?FuT>y^vP-&Q{GjUQCL>6<^SyzeW1U-{N=|2FvhA>}(i@?FaJe)OZtcYoja
zC?EX5A1mMggFm4B@Q-{<`OzQ$G36s4`E%tbKK_%+Pyg)CD4+P`&nv(1OTVam{HOj}
z`Q=~zl=A7%en$Dt&wg6@_22pp<ySuScgpYl-tQ`Z@JD~BeC~7qr2NUB{i*VofAx9g
zFFyZo%HMq9Z<R0p<KHX)@Wn4F|NO82qWt@p{zLh%|M_o)-pQT3Oev#htr3*R;HV@`
zl02iN@Y4k7l9J|><^>)mX$~i(fFgNLFrAVlDX>xzXOJ30QkK$^)qz^;6wc!ka<L-Y
zk(6bTBqpG-z~mBAOI8kBE90V_%PUF}=Ig>s%aHnHMOKt93261pGA`jHAYo~i9`&HG
zpOrrBN{^%wF~~YklOlk65mYFB5+?bYEJ2H#2ZZ`GEQ1oeOZ_tUAUP<YcIr|Rl7RTs
zBSFd9Ym+s0$XnumMSMcYx?~8{1q&oeMpra|%-lu&0nfv>mQa8M>kRttqt(z3c?dac
zXdFzJ@s1FJ!<}Mz50rxm<8%$20na3qc<o$y1mlp}BwvF@CrSJ~rXh466d^}Iv1dh0
z-H;U~D`=;H<0$tD^#FI3f&PH$S`z4e9)~4eLB$nl34_Gt-Os2KCw>uuZ9^!9X1Ad=
zuxW>bIcW;~J~4|`87I)B2`)#lVc?=xwgQ`DI|+1}M&Zii%o(-<V^Ks!yeb%12sQj7
zNkQQX8qGr(V=RLmf&ys-#q4B7%P`5JECmNG0vOw{!@;l`ElH6T%#CT0r!E|V-Dp9J
z4D)CqoZF`)#ya4|0m)Msh6M8Q00uIkML`obRp6&!`9)HSMlx^<I4^Ks!QjF`v(_-N
zRf4?SF=a*>11+~FOPu6{b)Q7(b|vD0bvg!RRiq8GWF6&dQl@EpDd-s4X(vxvCyFF*
z(Uf#yQM%PeOYLh)0S=WNH<UiJkU-;?Y)WtB-X6AMq|~IRcR1?H7BUn>{T|;>ZU<P(
zX%^tr2}iVV8i`3bVZx@+ig68JMys|y9ZfXdkyDohmZs_u<Eh7_F~M_gJ2sHT(wt95
z>f8wBxF)=Ym>B^(5zBJv8QRqJA?U>{xyX50BKJ)yf4FhzDT|b;aPq;r<OdkvOJ_Fh
zPpyO!o(Ai7ZuF{}eT!=oRr!ioh)mWmM;4x!W413MeqPFb1Iw&WG^Ga!@uoG4?|?QO
z$Rx`50WJHDs#EN*a71OqbT6wqV7g67wb$fn<4T0mr8DJVrJQ@ci9~_^Mm4RHYr%_|
zzQJo4^<FZ=PsGUpjz##Jn1&pmy<EJij)%OR(B-A((0yNWl>%|@IWmw1yhl9*D0;H>
z%7h4+Rega`;52AQqCN!|H)+DWAwWra$NJ0*=YmVHdNr+q$NSH;#H=c*nHB5X)!9NC
zU|JiaeOzL4ErUvu&7e@(PByPP3k-tG;9Q%Ua3w3sF}n*B4Y21uxX9(zbhyyY4|CQ0
zMs{sunrbAnk?b{2Ou;qXshXC|`aS0SnlOvSSpB;7l*3%fj&g^)$dYBJ4K8j&5hsHA
zPF2GFaZ1@x7dnRxaf`#fp7t8W_i>5Nt5Xzq7;^t%GeJ)7Y{qsomL`Hy;ILhv)#M|_
zdB8TSO$SGmFd7!ZNaC8hrj8DCiD{B<8q9b%d#sf3@yx2F2}cRIHtzS#*QUa63XZ~!
z<A`}~!R0f`#^OD?kIURx!s6|Sp;h4`-4qtjM4ARwQxOUR5mwn+zzzfMK`JEZij-XV
z@PP6lA628E?&T7ZX=s?Lw48LDO*qeFLw$83eI$^E(=5Hu+u^qNHglGnlMiIfJK57r
z5pA8lTr{gKkBVYq?5I+Dy5uT1rACsA+I2tfr^2Yk#y!OJVOr+pM5&8L_jAELOts#O
zVs1lWu~91H1eB5%TL~{>_jZe_8KV?7RhbGI6d2^1^|2(zW&{Q{Im88Ru03lpGre_{
zl6d;ujS1awfn#~b%*I<|9z^*)S{}LvTp$HlBC4gAypZ^Wh#e#1+BFy2+;n+Mhq$_>
z&h_Huj_Z!=c5>yl`=A#u3D-{K{y44N$({WFk}|$&f>wb$DE0PyQdbGPr0Lx@lNE58
z`#OI4Y}~4_81|I6-@C`Y6dxXGRk()WMeM8h1eenDZ<<F)I#k|v?_R~l@0V^~mWuM)
zy#|-kPi`8D7I1t1`n@eK{t?_w%DRsZzsseJZ_@qoPW;t-tz1ekxG}73ti1C2y$&wC
zg}oUpzjF9?E@iOu1>j4Na<*~!4ledF&P$eLP;>ZBE@jgFJ>*fuZa925m(uI_&iW`)
zOqveAhf7gj-j{)}O24*C)y1U@Uyl3ZD&1nNZZ7#X(Q&0(rJsRIQIGEaUYo-o<WhR;
z!>XT0NUOsi<`Q3$98KbGYwhP!jysZOhsz)4I&K^rDcxB6xr}3CmAi}2Q*!l=`VIYh
zpL^}M*HIm8uX`QfI;Ml|b*}?lM|80L?sb6chz=I*b*}?lM|80L?sbUkcsC-$z3z30
zOFtIY0r$H1w*6d3bFgrqd%ga~Td_9S3wA6AGrHaD{yXtsEYo5!+{1M&2kUgNSKqAu
zX>)0fdtF>da<HPqy<U5>`SFc6=^z^%hpXMaUVGQ>CnS;DZv{KfXDKc21<u~?%&FGq
zXdVBM{W)dFy<UIkTiy4Dg+AKhI*NmByVw199_T)?x2Hh=7+gWay<UCuY3^|RttoH}
z2h(fA_}aUk_8vN()~CQR9Bfc=z45MRiQL(2GX)ke9xE5_1yiu&bHK9eQ|Q7N9nZm5
z%)MUc>rIQqi=NUqJHDE)rofALFm$h1-|V<Z&uv(-#|Cb^4r}`CpR2z3??ML3_Ie|w
z^i>!k6KBF2jMkfV{D&=^0`nJ*l`D#Bhb_YpH&F!S?s$@PWlmnagAtv1NKZBVbmL*U
zJ=`6-+=B6()Dp{NDQh3&gJA42TabI>#WF4_=`xKl;v6Dx5ji|e4765jzc|UOmPFI-
z!pD>MLdRab6e$x=A5Xfbo{-!gsm3b3RbSxV$(`KEo&5h5?UqVU-m$=P*AoYOrCzRA
zOKq0f?0VZpcdM}MULy$_xs(4Ra&o;|&1#2~bmezwIq4r&cq1=FRg?7Oq?vqDD{-=0
z?4*%!!pKgx3X7b2J0}|*Cp(!{nI=oU^%~PjBm3l}QQ@RkbffH~4eq4Ycb?iTcqGy2
z;CdAYopLQ@*Wl_Vcu`<2l{7lM9@el+1_L82%UJFA&7#a4(C?RJzUU8AQuK!b96P+(
z!AxvW9Uk{5RPB%Bl4LU|VB{no^_w-Ajulh4hoFPYAdA5$Er%0o^+#(uoYHyUCZKrI
z?<dJ<FfH8vtjRSTGOjrcUgiz@ZV3gV!6=KiYM>6T?wPTM!`wX?4(D_+qocuKj?MPf
zfG~gwoeoY07B)K^48&%HP0W$Y%=;=Gu}L+f(?0Z&)fhmH!KMaR*VX!Hgd27+VrMJ?
z1U0dPIUTXhiCxf4XF4@RMOHXD;jOSzoNKU5PBdDAkdf>Z?oUJw!Nt^VAb9)yl$~|R
zjE;wbYs|Q&)SC?0(O^txkYSZPZ^1WAEz2!ju#Y+%>J-kW!=Xu_msO3)ka4M?jx#-*
z%xheu5wxvRedy2;Gfb4J!)f6IjXxSiG#HJ-a>#3yB`u)AG#ZVQ7z#wANgfVIwHkZ~
zRJuTQT{7+xmGyAQ%rP42bTWc@Sy-bIS{{v8aO{l%*UCw*I|a#f)HPye3)A6HjbSoR
zpq^b2*w<>TN{{K_>be2CB=eyOnCDwHfpAYH9j&a>eH|V#>t6#%HbIQhBrUforty9*
zu#I2rZFDqJ*W*K2lWsB6k|Z-$XuR2O4kue0I2TOAY#|HbZ&ewKOc)nVk^OByg>2F6
z?O9jntAY%RO~dEK+Ba&u_Ufb;MPUQWS(!`^aj6GHG$$On@@KK<qfslz+uoY!jfp?Q
znG|Qpb*SybTv9XNkHi5_?BJ?(VfPfK?f9@w_KE><At*@>z2v)*4c8Y-n0HgWWYDF4
zEQc{<G6_lHLG~<z_{0K-Uz&8h!1$uGbdtJQaBs8I{leOtQ+?!+vDi(iGgf1000P{|
zBTPRlr*f_ZU5rBrP{nGzBopA$sSX(xS00bmoN*m!dq0@L;;0spvDoPp1`Sd*s<P4H
z`ZxBP0XH1X=nTh7r&{TaYFrwo+Z3&QxOJ^@Qa@wuxv=z`lgX0pkPEG|;Fk=-=E7cp
zJaG#`C>FyRr_(H9Ja~=;<o<xU4t%UKokDvvy3(M3a~i-7OvOfR3I>@>Oo~_<=R)I5
z*c{NY(tN_W;tVr@XNq+}i$0mypyOcM`@tB2sd{v};5(B725z!=I!zO<o^u&APyqmn
z5p|kf;0}_L6qOisrK;;vo#x3@%TdSkU}>5OU2|aRcc+?*J4l#mngyFhN=Pyn_<}1e
zgK1cj!kKD%MoI@dFr8*8bi<^}g5;)#>3M)U_R8#lXlR+XO7#=YHC0>2#_Y7rp_UH_
zTIMjlh9iqut<|;JGjq(_1v6&&GqWi>nb%6QqlOlom^eK}>$OaaRkzA!d3iQxt#m=O
za&`BjEu%g>?WWmlv0Z>gi$K%TZ?t-on&hegno^HDv!(~)NVmBew_O*=4%kd+VcS};
zgX)Y~0!R`OgW1BSHWFnQSneifHuA8)I}G|l*Ku1rS<E#a_NF41%e52~aON%HJQz@0
z7hT5Hu_Oz#Z80Iot}NyzbyN)XzN(rS64gmiI+WF)&)mEusi940>#{6-79lW5M5I{Z
z5iCOq8d~I<_!Yq{W|>n6X*h3dc~ce&=dBz&T(f}2z~$3Sav9V#paC%V+$HvGk(CQA
zP3M|Xu6Qt?>eNspEErE`Ij#ih40Ab`$uo62$8%NWO`n-7OJO~prZMPXT=UY018`8_
zDgrQ@LtTR!(4tpPSqW`Hm;6Xqr!GNEnG0>}GLzY8L5=Aq5B2V18M^|5OU=?yr^GC9
zA<+z)r79fEu|D9;n3vjgq2Yq0u@c(U!^;Sbg;y{El$N<O@GqXpxvIRGSkmTbt`?9T
zE|+L=I5mkD#Z#sy$!Ac1f%O50a~bpu=D0^@98Kd*ouMt3WCl0dU|z_zsm_8l=fHJg
zu2TyFgGc75OM}f*VF6d1&YXN2q?%J=ci=b87H1R;Y3L4ev4E50#=<NkFdTN=pw2wN
zrjpsBNEUj^a@R28(PCjyXTdV(pidVyMP5RliVC>&BA73zo~>qZX#?HC9_5)!4Xkfc
zOK|CMeP2d8kRtg!BBb=-#LY=&LP5;E&<&TyP-qRAOPx?c98@D}x-=+A;kH4TJ7O3X
zwRJrUsI}1Z5Jt?Qbw?~BSP)F=Rw*up!IdRk+U~R>(`fhUU5Yjd2owzo+BeE|>{h+L
zM?Brg%1WEDqy{e`PP&`1Q}Y=xb|g(v2Tef%K2#YtoX6tXUQS5nRLTJbONiF+Xs`06
zRI^{KHi2ZB-BPZWf>_c5^-(eQX@7b-6Mvh8GfV!|(;8LS`58_VUCx)vb#%eEQGHgc
z$^On{+F5l}Z_T#P&g?TAgl)L66mfor`L#S-=3%nQX^@AnwF|youmEnD7O#9(skLBZ
zNyWA$bU}(G(i@G{j<)HleEfrE>IGmNadF^R0Q?d_UMFGb)eSIey+f6rx>#1DabY6M
zelD@x3<47bj>Q63n#{(qJD(SshH+juudm*D9<gnb7`mCqxT?5@m6s+i2PTEJhjTrb
z`s<5FEf_i%)`KDhck-wY$Fp^ouVB>#uIqQ(eGUPGndnj>)lFC;Fe;eHMf{+cORnc>
z;M!moOhY%N83cTcSuhV=I(HG%f<+a%o=g7{1h)9ItkSAYwPPjg)f{_?4-br4%xCe`
zfJHn_V4Yf{e_3bs(o)Z|p`@XHbG*EUY`Wr-GITJ4;`J*FsIXGfrD5XjfNogMwe;7;
zdqyz6Ur~qI$+C+WmK~S+OBxZ3r!=C5X<r}9!yk`vvBx(vK(^;6JdvmJ(tv~B%nB_@
z;+)wDah!kzmXnh+pOy(s05dIs%UKHTOWp>nvh-w&Qne?WscoN5n%082<+xm{4{?<g
z0T>Xj@JQ}CD@r{Z{NJb4v>ZXdZ3~gMuSFll!(X=X#S!d8Z}_g^r_iqLaIRWvE%?|b
zTg`DCn`LUV%;hHc*`H|67c}yyUARgtj~1#e4`qN&GAtPa9jCsC#9+kC0>4KGgUVtJ
zba*MKAok0>?fa|RwgI+u*hoM;*j$H!!ga12jEgtPEM)PkEw)`!-!RggtwR{(@Y3w8
zx?p3B?7-Q>wdxe$azO)dx%TD)5at?$OdCP~a@jZ=ZKM*{gPUu_C1oQi2UV}-1Kt^{
zxb#+g06M{z0R?L>mrhiYBBBrmFs{ReG>r}HtQTM!7{G_9nv>G7VHOk+z1)K27Zk27
zAYeEtTyQ`vgM8Bx`7m*dkXn}Obz=qa8?eoaKP`17&Q>dE7d#wXj7FA|NiG9o05Mq%
z2-uaqY5ACE!L3rP>kz(-GBquT6ToGjt*B?j$Y>d+%r7#`KeLv;Xp=**rUQZ1Eu0H3
zRUmrWu;E;I8|M{a&0ePEu{`8p0|vn#ZmPI(ZI6a{^XBa4Lzn%|#=|WM*e`B^z=ygp
zr;y8WVNAshSyb9_!6pQ7F9jFgpLuZGN(hBv1&3p4I4jmAq_K+I`S#LBfEq^7*mX$Y
zhiEU`2`G$NNFAS>r-)%>1Ds1C1#`eP?+W+03@vRdz;Y|w4zYcg2L*pS-?uH$Z7)~*
z2|Bn|A?sieQkb`E>aEZ`U>umnc?KTqGuFgiW&~DB+jfhLy_W!AGUSzX8PXJeFj^)%
z`O9TNZO7}ziaG_rBI;mhjPQ}cF<mhXC(Xh72?g`7A_~Kc?^;^>_|b#M)*^h+a7oIP
z25Xv;bcHKbT+0u6eoS2$#!i3>_>qSf7r3cT!{CDA674~CC>E|RC`1mA8y^aW(6Hyh
z)VY8z#1y6yq_xO}%wLHU6zCJzE1KRRx`-rMb#x&aW6GAV6{~&`g2^JfT1jP5h>eM}
zivkL;`jw8Tb{egVC}cWCGgvnWr8QXch7o?(dn8Jjf2B}eoPxGUobiOsSrqNnwVmIn
z`^+K?(>T4Rs%Mn=PVVH0@^ZZR*`*n|Dp_v=kT~CLYlV1<+|*(;KZW1*w(|A5p`aG)
zWT`Ebtw0!cm)awpr7xQKjR1eq2*zBtmW;4kDlX*jK55I|O?xU;d)v5f-8LSs@0uE)
z2jr--=Q{dEW&eL-x;eS&|A%%bcXB6(<m&v<`7Qhp^pEd@Jh**i5AQQMeR9(vTOzpb
z-#@>)X_SUkTriloiR}D-O&&aabbfyR_%=A()%_NEbdy9+9zA*tyGLzc=Ql=mb&BmH
z*uxgC2RDUvDZ&F1K4<~Ep|EjTZ~t)%*W(+(+BiFNq|>LhSA=gJo9X!SvV=?HkvfTI
z&StvBUQmYbibvTzJuDAQeWvNQzwUgZSY<9g^LAaoZ9)=v^NhzpybHDd6sGm&^>X8=
zdIjo~XBJNV>39RP{jp*X_j5gL9Yw9CxP{AC>M||G>+1M3V!iEW8T49TAG{ejHjd{_
zs`fE->S}knvaOaU;_aIF{=eGW4mZ;F#J-B_0b6<=AEuJ;OYP(`YPP7CL@uiZtMwV0
z6&D|{TFhtT`sC5+!$(_-Wl`zt4~M;7F6msORYplxz3Laas<+4XtT2sCnbq<z)JJs+
zS{ukm8}pqutH>8Ts^U>q+g!3WF+r<g1+JL{;d!~t+I>?{Evh!a9@edQ*{CA#isggU
zI=Fgr&0khrL~_U{nu&Mgw!A5YH~p|SR9Bae9zMKm7_VtC$t5SaIMrmn3<L7Hg?Ys#
z-vC=Z=UpbWJn8McBSxWAL8LJQB$s^P)^=mkw|ll2$)4m|iJZ+74Kps=4==mAyrPfK
zd8=2u*MilgXk1iuaP?$<zL|x675{N|#nnA*O?mP_4$;*)EZGk(xALoHykXD|uFV?;
zJ3f{%t2cZediBBs;X33|+j!ONb^0`1n`eH9txvr%<y%~Ngzc6qVTmV|U4+4&T=HmE
ztxAoHur99N#!E^^-9<BLD2@(1(C~^Iq88&XD;yRlE}zL>%AgU3NRg#6dmn0Fa><!l
zO%WolMYh)N2N)lo)RrD*rt`|xtLwz4@-Gn04nn^mB^OztS9z5TA+iH<f^I$OlF;Tg
zDjP1r$<|8|z#cSii}r9aN#xor0<T<rg3YUKRiRM4gSqX=p7kpmv=M38=yYR7CitIO
z<nv4Ue{qR-xmJzVOW8OK7Lm`39pmIJ<g%phV%4uunTaZ&V#Z_lB(?Cd%}~3Z(4?m)
zRqK7-ajO^ovdhA-s@GSC)zoNvk=}!*xoY-fRw9?@_299G%axa9Nz0qitJ%72$!ZO=
zwYz`P<mAqN-ne3I;S%pKS3L3FY;>z&Crb3>Dj)4U{kr|Jo{yRwwF|IQ%7?YS2(@s@
z%Q0~4U@W+3?u;8$`X&+-W}U2Wbv!nBbK~)(dCDE^VKb;=IFi?+{GZw6;x*0u)+bxp
zx|m#<XX&%aNS&K*)clx(#Ev!@_D56Q4!S=8k&gdx3-kVcVa|tJ6}IEVk>qjRTy?Hf
zJU`#Pv3*HgJa|43v#!R)FYtFipm<qa^zp;f>fWNp#Ys=MFMeJY*Q3US+swypx7tW*
z4vx=tb-(FB?bpuRk}P^h<~naKG}Su3GkjZ89QhS#=kiut$`xAd!OeWSRFliyKydZs
zHhhHtaA(9??u>4OoNrwR-V(XIe^Z}b?w1F*l<V|R?7AU2z2Pr8y5#)+>1}j-dUgB0
L8NHJ`c_H$@(}tcI
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/README
@@ -0,0 +1,5 @@
+This directory contains Trac's default clearsilver templates.
+
+Local modifications to these files might be lost during the installation of 
+a new Trac version. This can be avoided by making a copy of this entire
+directory before beginning modifications.
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/about.cs
@@ -0,0 +1,99 @@
+<?cs include "header.cs"?>
+<div id="ctxtnav" class="nav">
+ <h2>About Navigation</h2>
+ <ul>
+  <li class="first<?cs if:!about.config_href ?> last<?cs /if ?>"><a href="<?cs
+    var:trac.href.about ?>">Overview</a></li><?cs
+  if:about.config_href ?>
+   <li><a href="<?cs var:about.config_href ?>">Configuration</a></li><?cs
+  /if ?><?cs
+  if:about.plugins_href ?>
+   <li class="last"><a href="<?cs var:about.plugins_href ?>">Plugins</a></li><?cs
+  /if ?>
+ </ul>
+</div>
+<div id="content" class="about<?cs if:about.page ?>_<?cs var:about.page ?><?cs /if ?>">
+
+ <?cs if:about.page == "config"?>
+  <h1>Configuration</h1>
+  <table><thead><tr><th class="section">Section</th>
+   <th class="name">Name</th><th class="value">Value</th></tr></thead><?cs
+  each:section = about.config ?><?cs
+   if:len(section.options) ?>
+    <tr><th class="section" rowspan="<?cs var:len(section.options) ?>"><?cs var:section.name ?></th><?cs
+    each:option = section.options ?><?cs if:name(option) != 0 ?><tr><?cs /if ?>
+     <td class="name"><?cs var:option.name ?></td>
+     <td class="<?cs var:option.valueclass ?>"><?cs var:option.value ?></td>
+    </tr><?cs
+    /each ?><?cs
+   /if ?><?cs
+  /each ?></table>
+  <div id="help">
+   See <a href="<?cs var:trac.href.wiki ?>/TracIni">TracIni</a> for information about
+   the configuration.
+  </div>
+
+ <?cs elif:about.page == "plugins" ?>
+  <h1>Plugins</h1>
+  <dl id="plugins"><?cs
+   each:plugin = about.plugins ?>
+    <h2 id="<?cs var:plugin.module ?>.<?cs var:plugin.name ?>"><?cs var:plugin.name ?></h2>
+    <table>
+     <tr>
+      <th class="module" scope="row">Module</th>
+      <td class="module"><?cs var:plugin.module ?><br />
+      <span class="path"><?cs var:plugin.path ?></span></td>
+     </tr><?cs
+     if:plugin.description ?><tr>
+      <th class="description" scope="row">Description</th>
+      <td class="description"><?cs var:plugin.description ?></td>
+     </tr><?cs /if ?><?cs
+     if:len(plugin.extension_points) ?><tr>
+      <th class="xtnpts" rowspan="<?cs var:len(plugin.extension_points) ?>">
+       Extension points:</th><?cs
+       each:extension_point = plugin.extension_points ?><?cs
+        if:name(extension_point) != 0 ?><tr><?cs /if ?>
+        <td class="xtnpts">        
+         <code><?cs var:extension_point.module ?>.<?cs var:extension_point.interface ?></code><?cs
+          if:len(extension_point.extensions) ?> (<?cs
+           var:len(extension_point.extensions) ?> extensions)<ul><?cs
+           each:extension = extension_point.extensions ?>
+            <li><a href="#<?cs var:extension.module ?>.<?cs
+              var:extension.name ?>"><?cs var:extension.name ?></a></li><?cs
+           /each ?></ul><?cs
+          /if ?>
+          <div class="description"><?cs var:extension_point.description ?></div>
+        </td></tr><?cs
+       /each ?><?cs
+     /if ?>
+    </table><?cs
+   /each ?>
+  </dl>
+
+ <?cs else ?>
+  <a href="http://trac.edgewall.com" style="border: none; float: right; margin-left: 2em">
+   <img style="display: block" src="<?cs var:chrome.href ?>/common/trac_banner.png"
+     alt="Trac: Integrated SCM &amp; Project Management"/>
+  </a>
+<h1>About Trac <?cs var:trac.version ?></h1>
+<p>
+Trac is a web-based software project management and bug/issue
+tracking system emphasizing ease of use and low ceremony. 
+It provides an interface to the Subversion revision control systems, integrated Wiki and convenient report facilities. 
+</p>
+  <p>Trac is distributed under the modified BSD License.<br />
+  The complete text of the license can be found in the COPYING file
+  included in the distribution.</p>
+  <p>Please visit the Trac open source project: 
+  <a href="http://projects.edgewall.com/trac/">http://projects.edgewall.com/trac/</a></p>
+  <p>Trac is a product of <a href="http://www.edgewall.com/">Edgewall
+  Software</a>, provider of professional Linux and software development
+  services.</p>
+  <p>Copyright &copy; 2003-2006 <a href="http://www.edgewall.com/">Edgewall
+  Software</a></p>
+  <a href="http://www.edgewall.com/">
+   <img style="display: block; margin: 30px" src="<?cs var:chrome.href ?>/common/edgewall.png"
+     alt="Edgewall Software"/></a>
+ <?cs /if ?>
+</div>
+<?cs include "footer.cs"?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/anydiff.cs
@@ -0,0 +1,45 @@
+<?cs include "header.cs"?>
+
+<div id="ctxtnav" class="nav"></div>
+
+<div id="content" class="changeset">
+ <div id="title">
+    <h1>Select Base and Target for Diff:</h1>
+ </div>
+
+ <div id="anydiff">
+  <form action="<?cs var:anydiff.changeset_href ?>" method="get">
+   <table>
+    <tr>
+     <th><label for="old_path">From:</label></th>
+     <td>
+      <input type="text" id="old_path" name="old_path" value="<?cs
+         var:anydiff.old_path ?>" size="44" />
+      <label for="old_rev">at Revision:</label>
+      <input type="text" id="old_rev" name="old" value="<?cs
+         var:anydiff.old_rev ?>" size="4" />
+     </td>
+    </tr>
+    <tr>
+     <th><label for="new_path">To:</label></th>
+     <td>
+      <input type="text" id="new_path" name="new_path" value="<?cs
+         var:anydiff.new_path ?>" size="44" />
+      <label for="new_rev">at Revision:</label>
+      <input type="text" id="new_rev" name="new" value="<?cs
+         var:anydiff.new_rev ?>" size="4" />
+     </td>
+    </tr>
+   </table>
+   <div class="buttons">
+      <input type="submit" value="View changes" />
+   </div>
+  </form>
+ </div>
+ <div id="help">
+  <strong>Note:</strong> See <a href="<?cs var:trac.href.wiki
+  ?>/TracChangeset#ExaminingArbitraryDifferences">TracChangeset</a> for help on using the arbitrary diff feature.
+ </div>
+</div>
+
+<?cs include "footer.cs"?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/attachment.cs
@@ -0,0 +1,91 @@
+<?cs include "header.cs" ?>
+<?cs include "macros.cs" ?>
+
+<div id="ctxtnav" class="nav"></div>
+
+<div id="content" class="attachment">
+
+<?cs if:attachment.mode == 'new' ?>
+ <h1>Add Attachment to <a href="<?cs var:attachment.parent.href?>"><?cs
+   var:attachment.parent.name ?></a></h1>
+ <form id="attachment" method="post" enctype="multipart/form-data" action="">
+  <div class="field">
+   <label>File:<br /><input type="file" name="attachment" /></label>
+  </div>
+  <fieldset>
+   <legend>Attachment Info</legend>
+   <?cs if:trac.authname == "anonymous" ?>
+    <div class="field">
+     <label>Your email or username:<br />
+     <input type="text" name="author" size="30" value="<?cs
+       var:attachment.author?>" /></label>
+    </div>
+   <?cs /if ?>
+   <div class="field">
+    <label>Description of the file (optional):<br />
+    <input type="text" name="description" size="60" /></label>
+   </div>
+   <br />
+   <div class="options">
+    <label><input type="checkbox" name="replace" />
+    Replace existing attachment of the same name</label>
+   </div>
+   <br />
+  </fieldset>
+  <div class="buttons">
+   <input type="hidden" name="action" value="new" />
+   <input type="hidden" name="type" value="<?cs var:attachment.parent.type ?>" />
+   <input type="hidden" name="id" value="<?cs var:attachment.parent.id ?>" />
+   <input type="submit" value="Add attachment" />
+   <input type="submit" name="cancel" value="Cancel" />
+  </div>
+ </form>
+<?cs elif:attachment.mode == 'delete' ?>
+ <h1><a href="<?cs var:attachment.parent.href ?>"><?cs
+   var:attachment.parent.name ?></a>: <?cs var:attachment.filename ?></h1>
+ <p><strong>Are you sure you want to delete this attachment?</strong><br />
+ This is an irreversible operation.</p>
+ <div class="buttons">
+  <form method="post" action=""><div id="delete">
+   <input type="hidden" name="action" value="delete" />
+   <input type="submit" name="cancel" value="Cancel" />
+   <input type="submit" value="Delete attachment" />
+  </div></form>
+ </div>
+<?cs elif:attachment.mode == 'list' ?>
+ <h1><a href="<?cs var:attachment.parent.href ?>"><?cs
+   var:attachment.parent.name ?></a></h1><?cs
+  call:list_of_attachments(attachment.list, attachment.attach_href) ?>
+<?cs else ?>
+ <h1><a href="<?cs var:attachment.parent.href ?>"><?cs
+   var:attachment.parent.name ?></a>: <?cs var:attachment.filename ?></h1>
+ <table id="info" summary="Description"><tbody><tr>
+   <th scope="col">
+    File <?cs var:attachment.filename ?>, <?cs var:attachment.size ?> 
+    (added by <?cs var:attachment.author ?>,  <?cs var:attachment.age ?> ago)
+   </th></tr><tr>
+   <td class="message"><?cs var:attachment.description ?></td>
+  </tr>
+ </tbody></table>
+ <div id="preview"><?cs
+  if:attachment.preview ?>
+   <?cs var:attachment.preview ?><?cs
+  elif:attachment.max_file_size_reached ?>
+   <strong>HTML preview not available</strong>, since the file size exceeds
+   <?cs var:attachment.max_file_size  ?> bytes. You may <a href="<?cs
+     var:attachment.raw_href ?>">download the file</a> instead.<?cs
+  else ?>
+   <strong>HTML preview not available</strong>. To view the file,
+   <a href="<?cs var:attachment.raw_href ?>">download the file</a>.<?cs
+  /if ?>
+ </div>
+ <?cs if:attachment.can_delete ?><div class="buttons">
+  <form method="get" action=""><div id="delete">
+   <input type="hidden" name="action" value="delete" />
+   <input type="submit" value="Delete attachment" />
+  </div></form>
+ </div><?cs /if ?>
+<?cs /if ?>
+
+</div>
+<?cs include "footer.cs"?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/browser.cs
@@ -0,0 +1,147 @@
+<?cs include "header.cs"?>
+<?cs include "macros.cs"?>
+
+<div id="ctxtnav" class="nav">
+ <ul>
+  <li class="first"><a href="<?cs var:browser.restr_changeset_href ?>">
+   Last Change</a></li>
+  <li class="last"><a href="<?cs var:browser.log_href ?>">
+   Revision Log</a></li>
+ </ul>
+</div>
+
+
+<div id="searchable">
+<div id="content" class="browser">
+ <h1><?cs call:browser_path_links(browser.path, browser) ?></h1>
+
+ <div id="jumprev">
+  <form action="" method="get">
+   <div>
+    <label for="rev">View revision:</label>
+    <input type="text" id="rev" name="rev" value="<?cs
+       var:browser.revision ?>" size="4" />
+   </div>
+  </form>
+ </div>
+
+ <?cs def:sortable_th(order, desc, class, title, href) ?>
+ <th class="<?cs var:class ?><?cs if:order == class ?> <?cs
+   if:desc ?>desc<?cs else ?>asc<?cs /if ?><?cs /if ?>">
+  <a title="Sort by <?cs var:class ?><?cs
+    if:order == class && !desc ?> (descending)<?cs /if ?>" 
+     href="<?cs var:href[class] ?>"><?cs var:title ?></a>
+ </th>
+ <?cs /def ?>
+
+ <?cs if:browser.is_dir ?>
+  <table class="listing" id="dirlist">
+   <thead>
+    <tr><?cs 
+     call:sortable_th(browser.order, browser.desc, 'name', 'Name', browser.order_href) ?><?cs 
+     call:sortable_th(browser.order, browser.desc, 'size', 'Size', browser.order_href) ?>
+     <th class="rev">Rev</th><?cs 
+     call:sortable_th(browser.order, browser.desc, 'date', 'Age', browser.order_href) ?>
+     <th class="change">Last Change</th>
+    </tr>
+   </thead>
+   <tbody>
+    <?cs if:len(chrome.links.up) ?>
+     <tr class="even">
+      <td class="name" colspan="5">
+       <a class="parent" title="Parent Directory" href="<?cs
+         var:chrome.links.up.0.href ?>">../</a>
+      </td>
+     </tr>
+    <?cs /if ?>
+    <?cs each:item = browser.items ?>
+     <?cs set:change = browser.changes[item.rev] ?>
+     <tr class="<?cs if:name(item) % #2 ?>even<?cs else ?>odd<?cs /if ?>">
+      <td class="name"><?cs
+       if:item.is_dir ?><?cs
+        if:item.permission ?>
+         <a class="dir" title="Browse Directory" href="<?cs
+           var:item.browser_href ?>"><?cs var:item.name ?></a><?cs
+        else ?>
+         <span class="dir" title="Access Denied" href=""><?cs
+           var:item.name ?></span><?cs
+        /if ?><?cs
+       else ?><?cs
+        if:item.permission != '' ?>
+         <a class="file" title="View File" href="<?cs
+           var:item.browser_href ?>"><?cs var:item.name ?></a><?cs
+        else ?>
+         <span class="file" title="Access Denied" href=""><?cs
+           var:item.name ?></span><?cs
+        /if ?><?cs
+       /if ?>
+      </td>
+      <td class="size"><?cs var:item.size ?></td>
+      <td class="rev"><?cs if:item.permission != '' ?><a title="View Revision Log" href="<?cs
+        var:item.log_href ?>"><?cs var:item.rev ?></a><?cs else ?><?cs var:item.rev ?><?cs /if ?></td>
+      <td class="age"><span title="<?cs var:browser.changes[item.rev].date ?>"><?cs
+        var:browser.changes[item.rev].age ?></span></td>
+      <td class="change">
+       <span class="author"><?cs var:browser.changes[item.rev].author ?>:</span>
+       <span class="change"><?cs var:browser.changes[item.rev].message ?></span>
+      </td>
+     </tr>
+    <?cs /each ?>
+   </tbody>
+  </table><?cs
+ /if ?><?cs
+
+ if:len(browser.props) || !browser.is_dir ?>
+  <table id="info" summary="Revision info"><?cs
+   if:!browser.is_dir ?><tr>
+    <th scope="col">
+     Revision <a href="<?cs var:file.changeset_href ?>"><?cs var:file.rev ?></a>, <?cs var:file.size ?>
+     (checked in by <?cs var:file.author ?>, <?cs var:file.age ?> ago)
+    </th></tr><tr>
+    <td class="message"><?cs var:file.message ?></td>
+   </tr><?cs /if ?><?cs
+   if:len(browser.props) ?><tr>
+    <td colspan="2"><ul class="props"><?cs
+     each:prop = browser.props ?>
+      <li>Property <strong><?cs var:prop.name ?></strong> set to <em><code><?cs
+      var:prop.value ?></code></em></li><?cs
+     /each ?>
+    </ul></td></tr><?cs
+   /if ?>
+  </table><?cs
+ /if ?><?cs
+ 
+ if:!browser.is_dir ?>
+  <div id="preview"><?cs
+   if:file.preview ?><?cs
+    var:file.preview ?><?cs
+   elif:file.max_file_size_reached ?>
+    <strong>HTML preview not available</strong>, since the file size exceeds
+    <?cs var:file.max_file_size ?> bytes. Try <a href="<?cs
+    var:file.raw_href ?>">downloading</a> the file instead.<?cs
+   else ?><strong>HTML preview not available</strong>. To view, <a href="<?cs
+    var:file.raw_href ?>">download</a> the file.<?cs
+   /if ?>
+  </div><?cs
+ /if ?>
+
+ <div id="help">
+  <strong>Note:</strong> See <a href="<?cs var:trac.href.wiki
+  ?>/TracBrowser">TracBrowser</a> for help on using the browser.
+ </div>
+
+  <div id="anydiff">
+   <form action="<?cs var:browser.anydiff_href ?>" method="get">
+    <div class="buttons">
+     <input type="hidden" name="new_path" value="<?cs var:browser.path ?>" />
+     <input type="hidden" name="old_path" value="<?cs var:browser.path ?>" />
+     <input type="hidden" name="new_rev" value="<?cs var:browser.revision ?>" />
+     <input type="hidden" name="old_rev" value="<?cs var:browser.revision ?>" />
+     <input type="submit" value="View changes..." title="Prepare an Arbitrary Diff" />
+    </div>
+   </form>
+  </div>
+
+</div>
+</div>
+<?cs include:"footer.cs"?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/changeset.cs
@@ -0,0 +1,271 @@
+<?cs include "header.cs"?>
+<?cs include "macros.cs"?>
+
+<div id="ctxtnav" class="nav">
+ <h2>Navigation</h2><?cs
+ with:links = chrome.links ?>
+  <ul><?cs
+   if:changeset.chgset ?><?cs
+    if:changeset.restricted ?><?cs
+     set:change = "Change" ?><?cs
+    else ?><?cs 
+     set:change = "Changeset" ?><?cs
+    /if ?>
+    <li class="first"><?cs
+     if:len(links.prev) ?> &larr; 
+      <a class="prev" href="<?cs var:links.prev.0.href ?>" title="<?cs
+       var:links.prev.0.title ?>">Previous <?cs var:change ?></a> <?cs 
+     else ?>
+      <span class="missing">&larr; Previous <?cs var:change ?></span><?cs 
+     /if ?>
+    </li>
+    <li class="last"><?cs
+     if:len(links.next) ?>
+      <a class="next" href="<?cs var:links.next.0.href ?>" title="<?cs
+       var:links.next.0.title ?>">Next <?cs var:change ?></a> &rarr; <?cs 
+     else ?>
+      <span class="missing">Next <?cs var:change ?> &rarr;</span><?cs
+     /if ?>
+    </li><?cs
+   else ?>
+    <li class="last"><a href="<?cs var:changeset.reverse_href ?>">Reverse Diff</a></li><?cs
+   /if ?>
+  </ul><?cs
+ /with ?>
+</div>
+
+<div id="content" class="changeset">
+ <div id="title"><?cs
+  if:changeset.chgset ?><?cs
+   if:changeset.restricted ?>
+    <h1>Changeset <a title="Show full changeset" href="<?cs var:changeset.href.new_rev ?>">
+      <?cs var:changeset.new_rev ?></a> 
+     for <a title="Show entry in browser" href="<?cs var:changeset.href.new_path ?>">
+      <?cs var:changeset.new_path ?></a> 
+    </h1><?cs
+   else ?>
+    <h1>Changeset <?cs var:changeset.new_rev ?></h1><?cs
+   /if ?><?cs
+  else ?><?cs
+    if:changeset.restricted ?>
+    <h1>Changes in <a title="Show entry in browser" href="<?cs var:changeset.href.new_path ?>">
+      <?cs var:changeset.new_path ?></a>
+      <a title="Show revision log" href="<?cs var:changeset.href.log ?>">
+      [<?cs var:changeset.old_rev ?>:<?cs var:changeset.new_rev ?>]</a>
+    </h1><?cs
+   else ?>
+    <h1>Changes from <a title="Show entry in browser" href="<?cs var:changeset.href.old_path ?>">
+      <?cs var:changeset.old_path ?></a> 
+     at <a title="Show full changeset" href="<?cs var:changeset.href.old_rev ?>">
+      r<?cs var:changeset.old_rev ?></a>
+     to <a title="Show entry in browser" href="<?cs var:changeset.href.new_path ?>">
+     <?cs var:changeset.new_path ?></a> 
+     at <a title="Show full changeset" href="<?cs var:changeset.href.new_rev ?>">
+     r<?cs var:changeset.new_rev ?></a>
+    </h1><?cs
+   /if ?><?cs
+  /if ?>
+ </div>
+
+<?cs each:change = changeset.changes ?><?cs
+ if:len(change.diff) ?><?cs
+  set:has_diffs = 1 ?><?cs
+ /if ?><?cs
+/each ?><?cs if:has_diffs || diff.options.ignoreblanklines 
+  || diff.options.ignorecase || diff.options.ignorewhitespace ?>
+<form method="post" id="prefs" action="">
+ <div><?cs 
+  if:!changeset.chgset ?>
+   <input type="hidden" name="old_path" value="<?cs var:changeset.old_path ?>" />
+   <input type="hidden" name="new_path" value="<?cs var:changeset.new_path ?>" />
+   <input type="hidden" name="old" value="<?cs var:changeset.old_rev ?>" />
+   <input type="hidden" name="new" value="<?cs var:changeset.new_rev ?>" /><?cs
+  /if ?>
+  <label for="style">View differences</label>
+  <select id="style" name="style">
+   <option value="inline"<?cs
+     if:diff.style == 'inline' ?> selected="selected"<?cs
+     /if ?>>inline</option>
+   <option value="sidebyside"<?cs
+     if:diff.style == 'sidebyside' ?> selected="selected"<?cs
+     /if ?>>side by side</option>
+  </select>
+  <div class="field">
+   Show <input type="text" name="contextlines" id="contextlines" size="2"
+     maxlength="3" value="<?cs var:diff.options.contextlines ?>" />
+   <label for="contextlines">lines around each change</label>
+  </div>
+  <fieldset id="ignore">
+   <legend>Ignore:</legend>
+   <div class="field">
+    <input type="checkbox" id="blanklines" name="ignoreblanklines"<?cs
+      if:diff.options.ignoreblanklines ?> checked="checked"<?cs /if ?> />
+    <label for="blanklines">Blank lines</label>
+   </div>
+   <div class="field">
+    <input type="checkbox" id="case" name="ignorecase"<?cs
+      if:diff.options.ignorecase ?> checked="checked"<?cs /if ?> />
+    <label for="case">Case changes</label>
+   </div>
+   <div class="field">
+    <input type="checkbox" id="whitespace" name="ignorewhitespace"<?cs
+      if:diff.options.ignorewhitespace ?> checked="checked"<?cs /if ?> />
+    <label for="whitespace">White space changes</label>
+   </div>
+  </fieldset>
+  <div class="buttons">
+   <input type="submit" name="update" value="Update" />
+  </div>
+ </div>
+</form><?cs /if ?>
+
+<?cs def:node_change(item,cl,kind) ?><?cs 
+  set:ndiffs = len(item.diff) ?><?cs
+  set:nprops = len(item.props) ?>
+  <div class="<?cs var:cl ?>"></div><?cs 
+  if:cl == "rem" ?>
+   <a title="Show what was removed (rev. <?cs var:item.rev.old ?>)" href="<?cs
+     var:item.browser_href.old ?>"><?cs var:item.path.old ?></a><?cs
+  else ?>
+   <a title="Show entry in browser" href="<?cs
+     var:item.browser_href.new ?>"><?cs alt:item.path.new ?>(root)<?cs /alt?></a><?cs
+  /if ?>
+  <span class="comment">(<?cs var:kind ?>)</span><?cs
+  if:item.path.old && item.change == 'copy' || item.change == 'move' ?>
+   <small><em>(<?cs var:kind ?> from <a href="<?cs
+    var:item.browser_href.old ?>" title="Show original file (rev. <?cs
+    var:item.rev.old ?>)"><?cs var:item.path.old ?></a>)</em></small><?cs
+  /if ?><?cs
+  if:item.diff_href ?>
+    (<a href="<?cs var:item.diff_href ?>" title="Show differences">view diffs</a>)<?cs
+  elif:$ndiffs + $nprops > #0 ?>
+    (<a href="#file<?cs var:name(item) ?>" title="Show differences"><?cs
+      if:$ndiffs > #0 ?><?cs var:ndiffs ?>&nbsp;diff<?cs if:$ndiffs > #1 ?>s<?cs /if ?><?cs 
+      /if ?><?cs
+      if:$ndiffs && $nprops ?>, <?cs /if ?><?cs 
+      if:$nprops > #0 ?><?cs var:nprops ?>&nbsp;prop<?cs if:$nprops > #1 ?>s<?cs /if ?><?cs
+      /if ?></a>)<?cs
+  elif:cl == "mod" ?>
+    (<a href="<?cs var:item.browser_href.old ?>"
+        title="Show previous version in browser">previous</a>)<?cs
+  /if ?>
+<?cs /def ?>
+
+<dl id="overview"><?cs
+ if:changeset.chgset ?>
+ <dt class="property time">Timestamp:</dt>
+ <dd class="time"><?cs var:changeset.time ?> 
+  (<?cs alt:changeset.age ?>less than one hour<?cs /alt ?> ago)</dd>
+ <dt class="property author">Author:</dt>
+ <dd class="author"><?cs var:changeset.author ?></dd>
+ <?cs each:prop = changeset.properties ?>
+ <dt class="property <?cs var:prop.htmlclass ?>"><?cs var:prop.name ?>:</dt>
+ <dd class="<?cs var:prop.htmlclass ?>"><?cs var:prop.value ?></dd>
+ <?cs /each ?>
+ <dt class="property message">Message:</dt>
+ <dd class="message" id="searchable"><?cs
+  alt:changeset.message ?>&nbsp;<?cs /alt ?></dd><?cs
+ /if ?>
+ <dt class="property files"><?cs 
+  if:len(changeset.changes) > #0 ?>
+   Files:<?cs
+  else ?>
+   (No files)<?cs
+  /if ?>
+ </dt>
+ <dd class="files">
+  <ul><?cs each:item = changeset.changes ?>
+   <li><?cs
+    if:item.change == 'add' ?><?cs
+     call:node_change(item, 'add', 'added') ?><?cs
+    elif:item.change == 'delete' ?><?cs
+     call:node_change(item, 'rem', 'deleted') ?><?cs
+    elif:item.change == 'copy' ?><?cs
+     call:node_change(item, 'cp', 'copied') ?><?cs
+    elif:item.change == 'move' ?><?cs
+     call:node_change(item, 'mv', 'moved') ?><?cs
+    elif:item.change == 'edit' ?><?cs
+     call:node_change(item, 'mod', 'modified') ?><?cs
+    /if ?>
+   </li>
+  <?cs /each ?></ul>
+ </dd>
+</dl>
+
+<div class="diff">
+ <div id="legend">
+  <h3>Legend:</h3>
+  <dl>
+   <dt class="unmod"></dt><dd>Unmodified</dd>
+   <dt class="add"></dt><dd>Added</dd>
+   <dt class="rem"></dt><dd>Removed</dd>
+   <dt class="mod"></dt><dd>Modified</dd>
+   <dt class="cp"></dt><dd>Copied</dd>
+   <dt class="mv"></dt><dd>Moved</dd>
+  </dl>
+ </div>
+ <ul class="entries"><?cs
+ each:item = changeset.changes ?><?cs
+  if:len(item.diff) || len(item.props) ?><li class="entry" id="file<?cs
+   var:name(item) ?>"><h2><a href="<?cs
+   var:item.browser_href.new ?>" title="Show new revision <?cs
+   var:item.rev.new ?> of this file in browser"><?cs
+   var:item.path.new ?></a></h2><?cs
+   if:len(item.props) ?><ul class="props"><?cs
+    each:prop = item.props ?><li>Property <strong><?cs
+     var:prop.name ?></strong> <?cs
+     if:prop.old && prop.new ?>changed from <?cs
+     elif:!prop.old ?>set<?cs
+     else ?>deleted<?cs
+     /if ?><?cs
+     if:prop.old && prop.new ?><em><tt><?cs var:prop.old ?></tt></em><?cs /if ?><?cs
+     if:prop.new ?> to <em><tt><?cs var:prop.new ?></tt></em><?cs /if ?></li><?cs
+    /each ?></ul><?cs
+   /if ?><?cs
+   if:len(item.diff) ?><table class="<?cs
+    var:diff.style ?>" summary="Differences" cellspacing="0"><?cs
+    if:diff.style == 'sidebyside' ?>
+     <colgroup class="l"><col class="lineno" /><col class="content" /></colgroup>
+     <colgroup class="r"><col class="lineno" /><col class="content" /></colgroup>
+     <thead><tr>
+      <th colspan="2"><a href="<?cs
+       var:item.browser_href.old ?>" title="Show old rev. <?cs
+       var:item.rev.old ?> of <?cs var:item.path.old ?>">Revision <?cs
+       var:item.rev.old ?></a></th>
+      <th colspan="2"><a href="<?cs
+       var:item.browser_href.new ?>" title="Show new rev. <?cs
+       var:item.rev.new ?> of <?cs var:item.path.new ?>">Revision <?cs
+       var:item.rev.new ?></a></th>
+      </tr>
+     </thead><?cs
+     each:change = item.diff ?><tbody><?cs
+      call:diff_display(change, diff.style) ?></tbody><?cs
+      if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr>
+       <th>&hellip;</th><td>&nbsp;</td><th>&hellip;</th><td>&nbsp;</td>
+      </tr></tbody><?cs /if ?><?cs
+     /each ?><?cs
+    else ?>
+     <colgroup><col class="lineno" /><col class="lineno" /><col class="content" /></colgroup>
+     <thead><tr>
+      <th title="Revision <?cs var:item.rev.old ?>"><a href="<?cs
+       var:item.browser_href.old ?>" title="Show old version of <?cs
+       var:item.path.old ?>">r<?cs var:item.shortrev.old ?></a></th>
+      <th title="Revision <?cs var:item.rev.new ?>"><a href="<?cs
+       var:item.browser_href.new ?>" title="Show new version of <?cs
+       var:item.path.new ?>">r<?cs var:item.shortrev.new ?></a></th>
+      <th>&nbsp;</th></tr>
+     </thead><?cs
+     each:change = item.diff ?><?cs
+      call:diff_display(change, diff.style) ?><?cs
+      if:name(change) < len(item.diff) - 1 ?><tbody class="skipped"><tr>
+       <th>&hellip;</th><th>&hellip;</th><td>&nbsp;</td>
+      </tr></tbody><?cs /if ?><?cs
+     /each ?><?cs
+    /if ?></table><?cs
+   /if ?></li><?cs
+  /if ?><?cs
+ /each ?></ul>
+</div>
+
+</div>
+<?cs include "footer.cs"?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/error.cs
@@ -0,0 +1,36 @@
+<?cs include "header.cs"?>
+
+<div id="ctxtnav" class="nav"></div>
+
+<div id="content" class="error">
+
+ <?cs if error.type == "TracError" ?>
+  <h1><?cs var:error.title ?></h1>
+  <p class="message"><?cs var:error.message ?></p>
+
+ <?cs elif error.type == "internal" ?>
+  <h1>Oops&hellip;</h1>
+  <div class="message">
+   <strong>Trac detected an internal error:</strong>
+   <pre><?cs var:error.message ?></pre>
+  </div>
+  <p>If you think this really should work and you can reproduce it, you  should
+   consider reporting this problem to the Trac team.</p>
+  <p>Go to <a href="<?cs var:trac.href.homepage ?>"><?cs
+   var:trac.href.homepage ?></a> and create a new ticket where you describe
+   the problem, how to reproduce it. Don't forget to include the Python
+   traceback found below.</p>
+
+ <?cs /if ?>
+
+ <p>
+  <a href="<?cs var:trac.href.wiki ?>/TracGuide">TracGuide</a>
+  &mdash; The Trac User and Administration Guide
+ </p>
+ <?cs if:error.traceback ?>
+  <h4>Python Traceback</h4>
+  <pre><?cs var:error.traceback ?></pre>
+ <?cs /if ?>
+
+</div>
+<?cs include "footer.cs"?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/footer.cs
@@ -0,0 +1,35 @@
+<script type="text/javascript">searchHighlight()</script><?cs
+if:len(chrome.links.alternate) ?>
+<div id="altlinks"><h3>Download in other formats:</h3><ul><?cs
+ each:link = chrome.links.alternate ?><?cs
+  set:isfirst = name(link) == 0 ?><?cs
+  set:islast = name(link) == len(chrome.links.alternate) - 1?><li<?cs
+    if:isfirst || islast ?> class="<?cs
+     if:isfirst ?>first<?cs /if ?><?cs
+     if:isfirst && islast ?> <?cs /if ?><?cs
+     if:islast ?>last<?cs /if ?>"<?cs
+    /if ?>><a href="<?cs var:link.href ?>"<?cs if:link.class ?> class="<?cs
+    var:link.class ?>"<?cs /if ?>><?cs var:link.title ?></a></li><?cs
+ /each ?></ul></div><?cs
+/if ?>
+
+</div>
+
+<div id="footer">
+ <hr />
+ <a id="tracpowered" href="http://trac.edgewall.com/"><img src="<?cs
+   var:htdocs_location ?>trac_logo_mini.png" height="30" width="107"
+   alt="Trac Powered"/></a>
+ <p class="left">
+  Powered by <a href="<?cs var:trac.href.about ?>"><strong>Trac <?cs
+  var:trac.version ?></strong></a><br />
+  By <a href="http://www.edgewall.com/">Edgewall Software</a>.
+ </p>
+ <p class="right">
+  <?cs var:project.footer ?>
+ </p>
+</div>
+
+<?cs include "site_footer.cs" ?>
+ </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/header.cs
@@ -0,0 +1,74 @@
+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+<head><?cs
+ if:project.name_encoded ?>
+ <title><?cs if:title ?><?cs var:title ?> - <?cs /if ?><?cs
+   var:project.name_encoded ?> - Trac</title><?cs
+ else ?>
+ <title>Trac: <?cs var:title ?></title><?cs
+ /if ?><?cs
+ if:html.norobots ?>
+ <meta name="ROBOTS" content="NOINDEX, NOFOLLOW" /><?cs
+ /if ?><?cs
+ each:rel = chrome.links ?><?cs
+  each:link = rel ?><link rel="<?cs
+   var:name(rel) ?>" href="<?cs var:link.href ?>"<?cs
+   if:link.title ?> title="<?cs var:link.title ?>"<?cs /if ?><?cs
+   if:link.type ?> type="<?cs var:link.type ?>"<?cs /if ?> /><?cs
+  /each ?><?cs
+ /each ?><style type="text/css"><?cs include:"site_css.cs" ?></style><?cs
+ each:script = chrome.scripts ?>
+ <script type="<?cs var:script.type ?>" src="<?cs var:script.href ?>"></script><?cs
+ /each ?>
+</head>
+<body>
+<?cs include "site_header.cs" ?>
+<div id="banner">
+
+<div id="header"><?cs
+ if:chrome.logo.src ?><a id="logo" href="<?cs
+  var:chrome.logo.link ?>"><img src="<?cs var:chrome.logo.src ?>"<?cs
+  if:chrome.logo.width ?> width="<?cs var:chrome.logo.width ?>"<?cs /if ?><?cs
+  if:chrome.logo.height ?> height="<?cs var:chrome.logo.height ?>"<?cs
+  /if ?> alt="<?cs var:chrome.logo.alt ?>" /></a><hr /><?cs
+ elif:project.name_encoded ?><h1><a href="<?cs var:chrome.logo.link ?>"><?cs
+  var:project.name_encoded ?></a></h1><?cs
+ /if ?></div>
+
+<form id="search" action="<?cs var:trac.href.search ?>" method="get">
+ <?cs if:trac.acl.SEARCH_VIEW ?><div>
+  <label for="proj-search">Search:</label>
+  <input type="text" id="proj-search" name="q" size="10" accesskey="f" value="" />
+  <input type="submit" value="Search" />
+  <input type="hidden" name="wiki" value="on" />
+  <input type="hidden" name="changeset" value="on" />
+  <input type="hidden" name="ticket" value="on" />
+ </div><?cs /if ?>
+</form>
+
+<?cs def:nav(items) ?><?cs
+ if:len(items) ?><ul><?cs
+  set:idx = 0 ?><?cs
+  set:max = len(items) - 1 ?><?cs
+  each:item = items ?><?cs
+   set:first = idx == 0 ?><?cs
+   set:last = idx == max ?><li<?cs
+   if:first || last || item.active ?> class="<?cs
+    if:item.active ?>active<?cs /if ?><?cs
+    if:item.active && (first || last) ?> <?cs /if ?><?cs
+    if:first ?>first<?cs /if ?><?cs
+    if:(item.active || first) && last ?> <?cs /if ?><?cs
+    if:last ?>last<?cs /if ?>"<?cs
+   /if ?>><?cs var:item ?></li><?cs
+   set:idx = idx + 1 ?><?cs
+  /each ?></ul><?cs
+ /if ?><?cs
+/def ?>
+
+<div id="metanav" class="nav"><?cs call:nav(chrome.nav.metanav) ?></div>
+</div>
+
+<div id="mainnav" class="nav"><?cs call:nav(chrome.nav.mainnav) ?></div>
+<div id="main">
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/index.cs
@@ -0,0 +1,17 @@
+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
+<head><title>Available Projects</title></head>
+<body><h1>Available Projects</h1><ul><?cs
+ each:project = projects ?><li><?cs
+  if:project.href ?>
+   <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
+  /if ?>
+  </li><?cs
+ /each ?></ul></body>
+</html>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/log.cs
@@ -0,0 +1,176 @@
+<?cs include "header.cs"?>
+<?cs include "macros.cs"?>
+
+<div id="ctxtnav" class="nav">
+ <ul>
+  <li class="last">
+   <a href="<?cs var:log.browser_href ?>">View Latest Revision</a>
+  </li><?cs
+  if:len(chrome.links.prev) ?>
+   <li class="first<?cs if:!len(chrome.links.next) ?> last<?cs /if ?>">
+    &larr; <a href="<?cs var:chrome.links.prev.0.href ?>" title="<?cs
+      var:chrome.links.prev.0.title ?>">Newer Revisions</a>
+   </li><?cs
+  /if ?><?cs
+  if:len(chrome.links.next) ?>
+   <li class="<?cs if:!len(chrome.links.prev) ?>first <?cs /if ?>last">
+    <a href="<?cs var:chrome.links.next.0.href ?>" title="<?cs
+      var:chrome.links.next.0.title ?>">Older Revisions</a> &rarr;
+   </li><?cs
+  /if ?>
+ </ul>
+</div>
+
+
+<div id="content" class="log">
+ <h1><?cs call:browser_path_links(log.path, log) ?></h1>
+ <form id="prefs" action="<?cs var:browser_current_href ?>" method="get">
+  <div>
+   <input type="hidden" name="action" value="<?cs var:log.mode ?>" />
+   <label>View log starting at <input type="text" id="rev" name="rev" value="<?cs
+    var:log.items.0.rev ?>" size="5" /></label>
+   <label>and back to <input type="text" id="stop_rev" name="stop_rev" value="<?cs
+    var:log.stop_rev ?>" size="5" /></label>
+   <br />
+   <div class="choice">
+    <fieldset>
+     <legend>Mode:</legend>
+     <label for="stop_on_copy">
+      <input type="radio" id="stop_on_copy" name="mode" value="stop_on_copy" <?cs
+       if:log.mode != "follow_copy" || log.mode != "path_history" ?> checked="checked" <?cs
+       /if ?> />
+      Stop on copy 
+     </label>
+     <label for="follow_copy">
+      <input type="radio" id="follow_copy" name="mode" value="follow_copy" <?cs
+       if:log.mode == "follow_copy" ?> checked="checked" <?cs /if ?> />
+      Follow copies
+     </label>
+     <label for="path_history">
+      <input type="radio" id="path_history" name="mode" value="path_history" <?cs
+       if:log.mode == "path_history" ?> checked="checked" <?cs /if ?> />
+      Show only adds, moves and deletes
+     </label>
+    </fieldset>
+   </div>
+   <label><input type="checkbox" name="verbose" <?cs
+    if:log.verbose ?> checked="checked" <?cs
+    /if ?> /> Show full log messages</label>
+  </div>
+  <div class="buttons">
+   <input type="submit" value="Update" 
+          title="Warning: by updating, you will clear the page history" />
+  </div>
+ </form>
+
+ <div class="diff">
+  <div id="legend">
+   <h3>Legend:</h3>
+   <dl>
+    <dt class="add"></dt><dd>Added</dd><?cs
+    if:log.mode == "path_history" ?>
+     <dt class="rem"></dt><dd>Removed</dd><?cs
+    /if ?>
+    <dt class="mod"></dt><dd>Modified</dd>
+    <dt class="cp"></dt><dd>Copied or renamed</dd>
+   </dl>
+  </div>
+ </div>
+
+ <form  class="printableform" action="<?cs var:log.changeset_href ?>" method="get">
+  <div class="buttons"><input type="submit" value="View changes" 
+       title="Diff from Old Revision to New Revision (select them below)" />
+ </div>
+ <table id="chglist" class="listing">
+  <thead>
+   <tr>
+    <th class="diff"></th>
+    <th class="change"></th>
+    <th class="rev">Rev</th>
+    <th class="chgset">Chgset</th>
+    <th class="date">Date</th>
+    <th class="author">Author</th>
+    <th class="summary"><?cs if:!log.verbose ?>Log Message<?cs /if ?></th>
+   </tr>
+  </thead>
+  <tbody><?cs
+   set:indent = #1 ?><?cs
+   set:idx = #0 ?><?cs
+   each:item = log.items ?><?cs 
+    if:name(item) % #2 ?><?cs
+     set:even_odd = "odd" ?><?cs
+    else ?><?cs
+     set:even_odd = "even" ?><?cs
+    /if ?><?cs
+    if:item.copyfrom_path ?>
+     <tr class="<?cs var:even_odd ?>">
+      <td class="copyfrom_path" colspan="7" style="padding-left: <?cs var:indent ?>em">
+       copied from <a href="<?cs var:item.browser_href ?>"><?cs var:item.copyfrom_path ?></a>:
+      </td>
+     </tr><?cs
+     set:indent = indent + #1 ?><?cs
+    elif:log.mode == "path_history" ?><?cs
+      set:indent = #1 ?><?cs
+    /if ?>
+    <tr class="<?cs var:even_odd ?>">
+     <td class="diff">
+      <input type="radio" name="old" 
+             value="<?cs var:item.path ?>@<?cs var:item.rev ?>" <?cs
+          if:idx == #1 ?> checked="checked" <?cs /if ?> />
+      <input type="radio" name="new" 
+             value="<?cs var:item.path ?>@<?cs var:item.rev ?>" <?cs
+          if:idx == #0 ?> checked="checked" <?cs /if ?> /></td>
+     <td class="change" style="padding-left:<?cs var:indent ?>em">
+      <a title="View log starting at this revision" href="<?cs var:item.log_href ?>">
+       <span class="<?cs var:item.change ?>"></span>
+       <span class="comment">(<?cs var:item.change ?>)</span>
+      </a>
+     </td>
+     <td class="rev">
+      <a href="<?cs var:item.browser_href ?>" 
+         title="Browse at revision <?cs var:item.rev ?>">@<?cs var:item.rev ?></a>
+     </td>
+     <td class="chgset">
+      <a href="<?cs var:item.changeset_href ?>"
+         title="View changeset [<?cs var:item.rev ?>]">[<?cs var:item.rev ?>]</a>
+     </td>
+     <td class="date"><?cs var:log.changes[item.rev].date ?></td>
+     <td class="author"><?cs var:log.changes[item.rev].author ?></td>
+     <td class="summary"><?cs
+      if:!log.verbose ?><?cs var:log.changes[item.rev].message ?><?cs /if ?></td>
+    </tr><?cs
+    if:log.verbose ?>
+    <tr class="<?cs var:even_odd ?> verbose">
+     <td class="summary" colspan="7"><?cs var:log.changes[item.rev].message ?></td>
+    </tr><?cs
+    /if ?><?cs
+    set:idx = idx + 1 ?><?cs
+   /each ?>
+  </tbody>
+ </table><?cs
+ if:len(log.items) > #10 ?>
+  <div class="buttons"><input type="submit" value="View changes" 
+       title="Diff from Old Revision to New Revision (select them above)" />
+  </div><?cs
+ /if ?>
+ </form><?cs
+ if:len(links.prev) || len(links.next) ?><div id="paging" class="nav"><ul><?cs
+  if:len(links.prev) ?><li class="first<?cs
+   if:!len(links.next) ?> last<?cs /if ?>">&larr; <a href="<?cs
+   var:links.prev.0.href ?>" title="<?cs
+   var:links.prev.0.title ?>">Younger Revisions</a></li><?cs
+  /if ?><?cs
+  if:len(links.next) ?><li class="<?cs
+   if:len(links.prev) ?>first <?cs /if ?>last"><a href="<?cs
+   var:links.next.0.href ?>" title="<?cs
+   var:links.next.0.title ?>">Older Revisions</a> &rarr;</li><?cs
+  /if ?></ul></div><?cs
+ /if ?>
+
+ <div id="help">
+  <strong>Note:</strong> See <a href="<?cs var:trac.href.wiki
+  ?>/TracRevisionLog">TracRevisionLog</a> for help on using the revision log.
+ </div>
+
+</div>
+<?cs include "footer.cs"?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/log_changelog.cs
@@ -0,0 +1,25 @@
+#
+# ChangeLog for <?cs var:log.path ?> 
+# 
+# Generated by Trac <?cs var:trac.version ?>
+# <?cs var:trac.time ?>
+#
+<?cs each:item = $log.items ?>
+<?cs with:changeset = log.changes[item.rev] ?>
+<?cs var:changeset.date ?> <?cs
+     var:changeset.author ?> [<?cs var:item.rev ?>]<?cs
+  set:idx = 0 ?><?cs
+  each:file = changeset.files ?>
+	* <?cs var:file ?> (<?cs
+    set:action = changeset.actions[idx] ?><?cs
+    if:action == 'add' ?>added<?cs
+    elif:action == 'delete' ?>deleted<?cs
+    elif:action == 'copy' ?>copied<?cs
+    elif:action == 'move' ?>moved<?cs
+    elif:action == 'edit' ?>modified<?cs
+    /if ?>)<?cs
+    set:idx = idx + 1 ?><?cs
+  /each ?>
+<?cs var:changeset.message ?><?cs 
+  /with ?><?cs 
+/each ?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/log_rss.cs
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<!-- RSS generated by Trac v<?cs var:trac.version ?> on <?cs var:trac.time ?> -->
+<rss version="2.0">
+ <channel><?cs 
+  if:project.name_encoded ?>
+   <title><?cs var:project.name_encoded ?>: Revisions of <?cs var:log.path ?></title><?cs 
+  else ?>
+   <title>Revisions of <?cs var:log.path ?></title><?cs 
+  /if ?>
+  <link><?cs var:base_host ?><?cs var:log.log_href ?></link>
+  <description>Trac Log - Revisions of <?cs var:log.path ?></description>
+  <language>en-us</language>
+  <generator>Trac v<?cs var:trac.version ?></generator><?cs 
+  each:item = log.items ?><?cs 
+   with:change = log.changes[item.rev] ?>
+    <item><?cs
+     if:change.author ?><author><?cs var:change.author ?></author><?cs
+     /if ?>
+     <pubDate><?cs var:change.date ?></pubDate>
+     <title>Revision <?cs var:item.rev ?>: <?cs var:change.shortlog ?></title>
+     <link><?cs var:base_host ?><?cs var:item.restricted_href ?></link>
+     <description><?cs var:change.message ?></description>
+     <category>Log</category>
+    </item><?cs 
+   /with ?><?cs 
+  /each ?>
+ </channel>
+</rss>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/macros.cs
@@ -0,0 +1,197 @@
+<?cs def:hdf_select(options, name, selected, optional) ?>
+ <select size="1" id="<?cs var:name ?>" name="<?cs var:name ?>"><?cs
+  if:optional ?><option></option><?cs /if ?><?cs
+  each:option = options ?>
+   <option<?cs if:option == selected ?> selected="selected"<?cs /if ?>><?cs 
+     var:option ?></option><?cs
+  /each ?>
+ </select><?cs
+/def?><?cs
+
+def:labelled_hdf_select(label, options, name, selected, optional) ?><?cs 
+ if:len(options) > #0 ?>
+  <label for="<?cs var:name ?>"><?cs var:label ?></label><?cs
+   call:hdf_select(options, name, selected, optional) ?>
+  </label>
+  <br /><?cs
+ /if ?><?cs
+/def ?><?cs
+
+def:browser_path_links(path, file) ?><?cs
+ set:first = #1 ?><?cs
+  each:part = path ?><?cs
+   set:last = name(part) == len(path) - #1 ?><a<?cs 
+   if:first ?> class="first" title="Go to root directory"<?cs 
+    set:first = #0 ?><?cs 
+   else ?> title="View <?cs var:part.name ?>"<?cs
+   /if ?> href="<?cs var:part.href ?>"><?cs var:part.name ?></a><?cs
+   if:!last ?><span class="sep">/</span><?cs /if ?><?cs 
+ /each ?><?cs
+/def ?><?cs
+
+def:diff_line_class(block, line) ?><?cs
+ set:first = name(line) == 0 ?><?cs
+ set:last = name(line) + 1 == len(block.lines) ?><?cs
+ if:first || last ?> class="<?cs
+  if:first ?>first<?cs /if ?><?cs
+  if:first && last ?> <?cs /if ?><?cs
+  if:last ?>last<?cs /if ?>"<?cs
+ /if ?><?cs
+/def ?><?cs
+
+def:diff_display(diff, style) ?><?cs
+ if:style == 'sidebyside' ?><?cs
+  each:block = diff ?><?cs
+   if:block.type == 'unmod' ?><tbody><?cs
+    each:line = block.base.lines ?><tr><th><?cs
+     var:#block.base.offset + name(line) + 1 ?></th><td class="l"><span><?cs
+     var:line ?></span>&nbsp;</td><th><?cs
+     var:#block.changed.offset + name(line) + 1 ?></th><td class="r"><span><?cs
+     var:block.changed.lines[name(line)] ?></span>&nbsp;</td></tr><?cs
+    /each ?></tbody><?cs
+   elif:block.type == 'mod' ?><tbody class="mod"><?cs
+    if:len(block.base.lines) >= len(block.changed.lines) ?><?cs
+     each:line = block.base.lines ?><tr><th><?cs
+      var:#block.base.offset + name(line) + 1 ?></th><td class="l"><?cs
+      var:line ?>&nbsp;</td><?cs
+      if:len(block.changed.lines) >= name(line) + 1 ?><?cs
+       each:changedline = block.changed.lines ?><?cs
+        if:name(changedline) == name(line) ?><th><?cs
+         var:#block.changed.offset + name(changedline) + 1 ?></th><td class="r"><?cs
+         var:changedline ?>&nbsp;</td><?cs
+        /if ?><?cs
+       /each ?><?cs
+      else ?><th>&nbsp;</th><td class="r">&nbsp;</td><?cs
+      /if ?></tr><?cs
+     /each ?><?cs
+    else ?><?cs
+     each:line = block.changed.lines ?><tr><?cs
+      if:len(block.base.lines) >= name(line) + 1 ?><?cs
+       each:baseline = block.base.lines ?><?cs
+        if:name(baseline) == name(line) ?><th><?cs
+         var:#block.base.offset + name(baseline) + 1 ?></th><td class="l"><?cs
+         var:baseline ?>&nbsp;</td><?cs
+        /if ?><?cs
+       /each ?><?cs
+      else ?><th>&nbsp;</th><td class="l">&nbsp;</td><?cs
+      /if ?>
+      <th><?cs var:#block.changed.offset + name(line) + 1 ?></th>
+      <td class="r"><?cs var:line ?>&nbsp;</td></tr><?cs
+     /each ?><?cs
+    /if ?></tbody><?cs
+   elif:block.type == 'add' ?><tbody class="add"><?cs
+    each:line = block.changed.lines ?><tr><th>&nbsp;</th><td class="l">&nbsp;</td><th><?cs
+     var:#block.changed.offset + name(line) + 1 ?></th><td class="r"><ins><?cs
+     var:line ?></ins>&nbsp;</td></tr><?cs
+    /each ?><?cs
+   elif:block.type == 'rem' ?><tbody class="rem"><?cs
+    each:line = block.base.lines ?><tr><th><?cs
+     var:#block.base.offset + name(line) + 1 ?></th><td class="l"><del><?cs
+     var:line ?></del>&nbsp;</td><th>&nbsp;</th><td class="r">&nbsp;</td></tr><?cs
+    /each ?><?cs
+   /if ?></tbody><?cs
+  /each ?><?cs
+ else ?><?cs
+  each:block = diff ?><?cs
+   if:block.type == 'unmod' ?><tbody><?cs
+    each:line = block.base.lines ?><tr><th><?cs
+     var:#block.base.offset + name(line) + #1 ?></th><th><?cs
+     var:#block.changed.offset + name(line) + #1 ?></th><td class="l"><span><?cs
+     var:line ?></span>&nbsp;</td></tr><?cs
+    /each ?></tbody><?cs
+   elif:block.type == 'mod' ?><tbody class="mod"><?cs
+    each:line = block.base.lines ?><tr<?cs
+     if:name(line) == 0 ?> class="first"<?cs /if ?>><th><?cs
+     var:#block.base.offset + name(line) + #1 ?></th><th>&nbsp;</th><td class="l"><?cs
+     var:line ?>&nbsp;</td></tr><?cs
+    /each ?><?cs
+    each:line = block.changed.lines ?><tr<?cs
+     if:name(line) + 1 == len(block.changed.lines) ?> class="last"<?cs /if ?>><th>&nbsp;</th><th><?cs
+     var:#block.changed.offset + name(line) + #1 ?></th><td class="r"><?cs
+     var:line ?>&nbsp;</td></tr><?cs
+    /each ?></tbody><?cs
+   elif:block.type == 'add' ?><tbody class="add"><?cs
+    each:line = block.changed.lines ?><tr<?cs
+     call:diff_line_class(block.changed, line) ?>><th>&nbsp;</th><th><?cs
+     var:#block.changed.offset + name(line) + #1 ?></th><td class="r"><ins><?cs
+     var:line ?></ins>&nbsp;</td></tr><?cs
+    /each ?></tbody><?cs
+   elif:block.type == 'rem' ?><tbody class="rem"><?cs
+    each:line = block.base.lines ?><tr<?cs
+     call:diff_line_class(block.base, line) ?>><th><?cs
+     var:#block.base.offset + name(line) + 1 ?></th><th>&nbsp;</th><td class="l"><del><?cs
+     var:line ?></del>&nbsp;</td></tr><?cs
+    /each ?></tbody><?cs
+   /if ?><?cs
+  /each ?><?cs
+ /if ?><?cs
+/def ?><?cs
+
+def:ticket_custom_props(ticket) ?><?cs
+ each c=ticket.custom ?>
+  <div class="field custom_<?cs var c.name ?>"><?cs
+   if c.type == 'text' ?>
+    <label>
+     <?cs alt c.label ?><?cs var c.name ?><?cs /alt ?>:
+     <input type="text" name="custom_<?cs var c.name ?>" value="<?cs var c.value ?>" />
+    </label><?cs
+   elif c.type == 'textarea' ?>
+    <label>
+     <?cs alt c.label ?><?cs var c.name ?><?cs /alt ?>:<br />
+     <textarea cols="<?cs alt c.width ?>60<?cs /alt ?>" rows="<?cs
+       alt c.height ?>12<?cs /alt ?>" name="custom_<?cs var c.name ?>"><?cs
+       var c.value ?></textarea>
+    </label><?cs
+   elif c.type == 'checkbox' ?>
+    <input type="hidden" name="checkbox_<?cs var c.name ?>" />
+    <label>
+     <input type="checkbox" name="custom_<?cs var c.name ?>" value="1"<?cs
+       if c.selected ?> checked="checked"<?cs /if ?> />
+     <?cs alt c.label ?><?cs var c.name ?><?cs /alt ?>
+    </label><?cs
+   elif c.type == 'select' ?>
+    <label>
+     <?cs alt c.label ?><?cs var c.name ?><?cs /alt ?>:
+     <select name="custom_<?cs var c.name ?>"><?cs each v = c.option ?>
+      <option<?cs if v.selected ?> selected="selected"<?cs /if ?>><?cs
+        var v ?></option><?cs /each ?>
+     </select>
+    </label><?cs
+   elif c.type == 'radio' ?>
+    <fieldset class="radio">
+     <legend><?cs alt c.label ?><?cs var c.name ?><?cs /alt ?>:</legend><?cs
+     each v = c.option ?>
+      <label><input type="radio" name="custom_<?cs var c.name ?>" value="<?cs
+         var v ?>"<?cs if v.selected ?> checked="checked"<?cs /if ?> /> <?cs
+         var v ?></label><?cs
+     /each ?>
+    </fieldset><?cs
+   /if ?>
+  </div><?cs
+ /each ?><?cs
+/def ?><?cs 
+
+def:list_of_attachments(attachments, attach_href) ?>
+<h2>Attachments</h2><?cs
+ if:len(attachments) ?><div id="attachments">
+  <dl class="attachments"><?cs each:attachment = attachments ?>
+   <dt><a href="<?cs var:attachment.href ?>" title="View attachment"><?cs
+   var:attachment.filename ?></a> (<?cs var:attachment.size ?>) - added by <em><?cs
+   var:attachment.author ?></em> on <?cs
+   var:attachment.time ?>.</dt><?cs
+   if:attachment.description ?>
+    <dd><?cs var:attachment.description ?></dd><?cs
+   /if ?><?cs
+  /each ?></dl><?cs
+ /if ?><?cs
+ if:attach_href ?>
+  <form method="get" action="<?cs var:attach_href ?>"><div>
+   <input type="hidden" name="action" value="new" />
+   <input type="submit" value="Attach File" />
+  </div></form><?cs
+ /if ?><?cs if:len(attachments) ?></div><?cs /if ?><?cs
+/def ?><?cs
+
+def:plural(base, count) ?><?cs
+ var:base ?><?cs if:count != 1 ?>s<?cs /if ?><?cs
+/def ?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/milestone.cs
@@ -0,0 +1,244 @@
+<?cs include:"header.cs"?>
+<?cs include:"macros.cs"?>
+
+<div id="ctxtnav" class="nav"></div>
+
+<div id="content" class="milestone">
+ <?cs if:milestone.mode == "new" ?>
+ <h1>New Milestone</h1>
+ <?cs elif:milestone.mode == "edit" ?>
+ <h1>Edit Milestone <?cs var:milestone.name ?></h1>
+ <?cs elif:milestone.mode == "delete" ?>
+ <h1>Delete Milestone <?cs var:milestone.name ?></h1>
+ <?cs else ?>
+ <h1>Milestone <?cs var:milestone.name ?></h1>
+ <?cs /if ?>
+
+ <?cs if:milestone.mode == "edit" || milestone.mode == "new" ?>
+  <script type="text/javascript">
+    addEvent(window, 'load', function() {
+      document.getElementById('name').focus();
+    });
+  </script>
+  <form id="edit" action="<?cs var:milestone.href ?>" method="post">
+   <input type="hidden" name="id" value="<?cs var:milestone.name ?>" />
+   <input type="hidden" name="action" value="edit" />
+   <div class="field">
+    <label>Name of the milestone:<br />
+    <input type="text" id="name" name="name" size="32" value="<?cs
+      var:milestone.name ?>" /></label>
+   </div>
+   <fieldset>
+    <legend>Schedule</legend>
+    <label>Due:<br />
+     <input type="text" id="duedate" name="duedate" size="<?cs
+       var:len(milestone.date_hint) ?>" value="<?cs
+       var:milestone.due_date ?>" title="Format: <?cs var:milestone.date_hint ?>" />
+     <em>Format: <?cs var:milestone.date_hint ?></em>
+    </label>
+    <div class="field">
+     <label>
+      <input type="checkbox" id="completed" name="completed"<?cs
+        if:milestone.completed ?> checked="checked"<?cs /if ?> />
+      Completed:<br />
+     </label>
+     <label>
+      <input type="text" id="completeddate" name="completeddate" size="<?cs
+        var:len(milestone.date_hint) ?>" value="<?cs
+        alt:milestone.completed_date ?><?cs
+         var:milestone.datetime_now ?><?cs
+        /alt ?>" title="Format: <?cs
+        var:milestone.datetime_hint ?>" />
+      <em>Format: <?cs var:milestone.datetime_hint ?></em>
+     </label><?cs
+     if:len(milestones) ?>
+     <br/>
+     <input type="checkbox" id="retarget" name="retarget" checked="checked"
+            onclick="enableControl('target', this.checked)"/>
+     <label>
+      Retarget associated open tickets to milestone
+      <select id="target" name="target">
+       <option value="">None</option><?cs
+       each:name = milestones ?>
+       <option><?cs var:name ?></option><?cs
+       /each ?>
+      </select>
+     </label><?cs
+     /if ?>
+     <script type="text/javascript">
+       var completed = document.getElementById("completed");
+       var retarget = document.getElementById("retarget");
+       var enableCompletedDate = function() {
+         enableControl("completeddate", completed.checked);
+         enableControl("retarget", completed.checked);
+         enableControl("target", completed.checked && retarget.checked);
+       };
+       addEvent(window, "load", enableCompletedDate);
+       addEvent(completed, "click", enableCompletedDate);
+     </script>
+    </div>
+   </fieldset>
+   <div class="field">
+    <fieldset class="iefix">
+     <label for="description">Description (you may use <a tabindex="42" href="<?cs
+       var:trac.href.wiki ?>/WikiFormatting">WikiFormatting</a> here):</label>
+     <p><textarea id="description" name="description" class="wikitext" rows="10" cols="78">
+<?cs var:milestone.description_source ?></textarea></p>
+    </fieldset>
+   </div>
+   <div class="buttons">
+    <?cs if:milestone.mode == "new"
+     ?><input type="submit" value="Add milestone" /><?cs
+    else
+     ?><input type="submit" value="Submit changes" /><?cs
+    /if ?>
+    <input type="submit" name="cancel" value="Cancel" />
+   </div>
+   <script type="text/javascript" src="<?cs
+     var:htdocs_location ?>js/wikitoolbar.js"></script>
+  </form>
+ <?cs elif:milestone.mode == "delete" ?>
+  <form action="<?cs var:milestone.href ?>" method="post">
+   <input type="hidden" name="id" value="<?cs var:milestone.name ?>" />
+   <input type="hidden" name="action" value="delete" />
+   <p><strong>Are you sure you want to delete this milestone?</strong></p>
+   <input type="checkbox" id="retarget" name="retarget" checked="checked"
+       onclick="enableControl('target', this.checked)"/>
+   <label for="target">Retarget associated tickets to milestone</label>
+   <select name="target" id="target">
+    <option value="">None</option><?cs
+     each:other = milestones ?><?cs if:other != milestone.name ?>
+      <option><?cs var:other ?></option><?cs 
+     /if ?><?cs /each ?>
+   </select>
+   <div class="buttons">
+    <input type="submit" name="cancel" value="Cancel" />
+    <input type="submit" value="Delete milestone" />
+   </div>
+  </form>
+ <?cs else ?>
+ <?cs if:milestone.mode == "view" ?>
+  <div class="info">
+   <p class="date"><?cs
+    if:milestone.completed_date ?>
+     Completed <?cs var:milestone.completed_delta ?> ago (<?cs var:milestone.completed_date ?>)<?cs
+    elif:milestone.due_date ?><?cs
+     if:milestone.late ?>
+      <strong><?cs var:milestone.due_delta ?> late</strong><?cs
+     else ?>
+      Due in <?cs var:milestone.due_delta ?><?cs
+     /if ?> (<?cs var:milestone.due_date ?>)<?cs
+    else ?>
+     No date set<?cs
+    /if ?>
+   </p><?cs
+   with:stats = milestone.stats ?><?cs
+    if:#stats.total_tickets > #0 ?>
+     <table class="progress">
+      <tr>
+      <td class="closed" style="width: <?cs
+        var:#stats.percent_closed ?>%">
+        <a href="<?cs
+        var:milestone.queries.closed_tickets ?>" title="<?cs
+        var:#stats.closed_tickets ?> of <?cs
+        var:#stats.total_tickets ?> ticket<?cs
+        if:#stats.total_tickets != #1 ?>s<?cs /if ?> closed"></a></td>
+      <td class="open" style="width: <?cs
+        var:#stats.percent_active ?>%">
+        <a href="<?cs
+        var:milestone.queries.active_tickets ?>" title="<?cs
+        var:#stats.active_tickets ?> of <?cs
+        var:#stats.total_tickets ?> ticket<?cs
+        if:#stats.total_tickets != #1 ?>s<?cs /if ?> active"></a>
+      </tr>
+     </table>
+     <p class="percent"><?cs var:#stats.percent_closed ?>%</p>
+     <dl>
+      <dt>Closed tickets:</dt>
+      <dd><a href="<?cs var:milestone.queries.closed_tickets ?>"><?cs
+        var:stats.closed_tickets ?></a></dd>
+      <dt>Active tickets:</dt>
+      <dd><a href="<?cs var:milestone.queries.active_tickets ?>"><?cs
+        var:stats.active_tickets ?></a></dd>
+     </dl><?cs
+    /if ?><?cs
+   /with ?>
+  </div>
+  <form id="stats" action="" method="get">
+   <fieldset>
+    <legend>
+     <label for="by">Ticket status by</label>
+     <select id="by" name="by" onchange="this.form.submit()"><?cs
+     each:group = milestone.stats.available_groups ?>
+      <option value="<?cs var:group.name ?>" <?cs
+        if:milestone.stats.grouped_by == group.name ?> selected="selected"<?cs
+        /if ?>><?cs var:group.label ?></option><?cs
+     /each ?></select>
+     <noscript><input type="submit" value="Update" /></noscript>
+    </legend>
+    <table summary="Shows the milestone completion status grouped by <?cs
+      var:milestone.stats.grouped_by ?>"><?cs
+     each:group = milestone.stats.groups ?>
+      <tr>
+       <th scope="row"><a href="<?cs
+         var:group.queries.all_tickets ?>"><?cs var:group.name ?></a></th>
+       <td style="white-space: nowrap"><?cs if:#group.total_tickets ?>
+        <table class="progress" style="width: <?cs
+          var:#group.percent_total * #80 / #milestone.stats.max_percent_total ?>%">
+         <tr>
+          <td class="closed" style="width: <?cs
+            var:#group.percent_closed ?>%"><a href="<?cs
+            var:group.queries.closed_tickets ?>" title="<?cs
+           var:group.closed_tickets ?> of <?cs
+           var:group.total_tickets ?> ticket<?cs
+           if:group.total_tickets != #1 ?>s<?cs /if ?> closed"></a>
+          </td>
+          <td class="open" style="width: <?cs
+            var:#group.percent_active ?>%"><a href="<?cs
+            var:group.queries.active_tickets ?>" title="<?cs
+           var:group.active_tickets ?> of <?cs
+           var:group.total_tickets ?> ticket<?cs
+           if:group.total_tickets != 1 ?>s<?cs /if ?> active"></a>
+          </td>
+         </tr>
+        </table>
+        <p class="percent"><?cs var:group.closed_tickets ?>/<?cs
+         var:group.total_tickets ?></p>
+       <?cs /if ?></td>
+      </tr><?cs
+     /each ?>
+    </table><?cs /if ?>
+   </fieldset>
+  </form>
+  <div class="description"><?cs var:milestone.description ?></div><?cs
+  if:trac.acl.MILESTONE_MODIFY || trac.acl.MILESTONE_DELETE ?>
+   <div class="buttons"><?cs
+    if:trac.acl.MILESTONE_MODIFY ?>
+     <form method="get" action=""><div>
+      <input type="hidden" name="action" value="edit" /><?cs
+      if:milestone.id_param ?>
+       <input type="hidden" name="id" value="<?cs var:milestone.name ?>" /><?cs
+      /if ?>
+      <input type="submit" value="Edit milestone info" accesskey="e" />
+     </div></form><?cs
+    /if ?><?cs
+    if:trac.acl.MILESTONE_DELETE ?>
+     <form method="get" action=""><div>
+      <input type="hidden" name="action" value="delete" /><?cs
+      if:milestone.id_param ?>
+       <input type="hidden" name="id" value="<?cs var:milestone.name ?>" /><?cs
+      /if ?>
+      <input type="submit" value="Delete milestone" />
+     </div></form><?cs
+    /if ?>
+   </div><?cs
+  /if ?><?cs
+ /if ?>
+
+ <div id="help">
+  <strong>Note:</strong> See <a href="<?cs
+    var:trac.href.wiki ?>/TracRoadmap">TracRoadmap</a> for help on using the roadmap.
+ </div>
+
+</div>
+<?cs include:"footer.cs"?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/newticket.cs
@@ -0,0 +1,125 @@
+<?cs include:"header.cs" ?>
+<?cs include:"macros.cs" ?>
+<script type="text/javascript">
+addEvent(window, 'load', function() { document.getElementById('summary').focus()}); 
+</script>
+
+<div id="ctxtnav" class="nav"></div>
+
+<div id="content" class="ticket">
+<h1>Create New Ticket</h1>
+<?cs include:"site_newticket.cs" ?>
+<form id="newticket" method="post" action="<?cs
+  var:trac.href.newticket ?>#preview">
+ <?cs if:trac.authname == "anonymous" ?>
+  <div class="field">
+   <label for="reporter">Your email or username:</label><br />
+   <input type="text" id="reporter" name="reporter" size="40" value="<?cs
+     var:newticket.reporter ?>" /><br />
+  </div>
+ <?cs /if ?>
+ <div class="field">
+  <label for="summary">Short summary:</label><br />
+  <input id="summary" type="text" name="summary" size="80" value="<?cs
+    var:newticket.summary ?>"/>
+ </div><?cs
+ if:len(newticket.fields.type.options) ?>
+  <div class="field"><label for="type">Type:</label> <?cs
+   call:hdf_select(newticket.fields.type.options, 'type',
+                   newticket.type, 0) ?>
+  </div><?cs
+ /if ?>
+ <div class="field">
+  <label for="description">Full description (you may use <a tabindex="42" href="<?cs
+    var:$trac.href.wiki ?>/WikiFormatting">WikiFormatting</a> here):</label><br />
+  <textarea id="description" name="description" class="wikitext" rows="10" cols="78">
+<?cs var:newticket.description ?></textarea><?cs
+  if:newticket.description_preview ?>
+   <fieldset id="preview">
+    <legend>Description Preview</legend>
+    <?cs var:newticket.description_preview ?>
+   </fieldset><?cs
+  /if ?>
+ </div>
+
+ <fieldset id="properties">
+  <legend>Ticket Properties</legend>
+  <input type="hidden" name="action" value="create" />
+  <input type="hidden" name="status" value="new" />
+  <table><tr><?cs set:num_fields = 0 ?><?cs
+  each:field = newticket.fields ?><?cs
+   if:!field.skip ?><?cs
+    set:num_fields = num_fields + 1 ?><?cs
+   /if ?><?cs
+  /each ?><?cs set:idx = 0 ?><?cs
+   each:field = newticket.fields ?><?cs
+    if:!field.skip ?><?cs set:fullrow = field.type == 'textarea' ?><?cs
+     if:fullrow && idx % 2 ?><?cs set:idx = idx + 1 ?><th class="col2"></th><td></td></tr><tr><?cs /if ?>
+     <th class="col<?cs var:idx % 2 + 1 ?>"><?cs
+       if:field.type != 'radio' ?><label for="<?cs var:name(field) ?>"><?cs
+       /if ?><?cs alt:field.label ?><?cs var:field.name ?><?cs /alt ?>:<?cs
+       if:field.type != 'radio' ?></label><?cs /if ?></th>
+     <td<?cs if:fullrow ?> colspan="3"<?cs /if ?>><?cs
+      if:field.type == 'text' ?><input type="text" id="<?cs
+        var:name(field) ?>" name="<?cs
+        var:name(field) ?>" value="<?cs var:newticket[name(field)] ?>" /><?cs
+      elif:field.type == 'select' ?><select id="<?cs
+        var:name(field) ?>" name="<?cs var:name(field) ?>"><?cs
+        if:field.optional ?><option></option><?cs /if ?><?cs
+        each:option = field.options ?><option<?cs
+         if:option == newticket[name(field)] ?> selected="selected"<?cs /if ?>><?cs
+         var:option ?></option><?cs
+        /each ?></select><?cs
+      elif:field.type == 'checkbox' ?><input type="hidden" name="checkbox_<?cs
+        var:name(field) ?>" /><input type="checkbox" id="<?cs
+        var:name(field) ?>" name="<?cs
+        var:name(field) ?>" value="1"<?cs
+        if:newticket[name(field)] ?> checked="checked"<?cs /if ?> /><?cs
+      elif:field.type == 'textarea' ?><textarea id="<?cs
+        var:name(field) ?>" name="<?cs
+        var:name(field) ?>"<?cs
+        if:field.height ?> rows="<?cs var:field.height ?>"<?cs /if ?><?cs
+        if:field.width ?> cols="<?cs var:field.width ?>"<?cs /if ?>><?cs
+        var:newticket[name(field)] ?></textarea><?cs
+      elif:field.type == 'radio' ?><?cs set:optidx = 0 ?><?cs
+       each:option = field.options ?><label><input type="radio" id="<?cs
+         var:name(field) ?>" name="<?cs
+         var:name(field) ?>" value="<?cs var:option ?>"<?cs
+         if:ticket[name(field)] == option ?> checked="checked"<?cs /if ?> /> <?cs
+         var:option ?></label> <?cs set:optidx = optidx + 1 ?><?cs
+       /each ?><?cs
+      /if ?></td><?cs
+     if:idx % 2 || fullrow ?><?cs
+      if:idx < num_fields - 1 ?></tr><tr><?cs
+      /if ?><?cs 
+     elif:idx == num_fields - 1 ?><th class="col2"></th><td></td><?cs
+     /if ?><?cs set:idx = idx + #fullrow + 1 ?><?cs
+    /if ?><?cs
+   /each ?></tr>
+  </table>
+ </fieldset>
+
+ <script type="text/javascript" src="<?cs
+   var:htdocs_location ?>js/wikitoolbar.js"></script>
+
+ <?cs if newticket.can_attach ?><p>
+  <label><input type="checkbox" name="attachment"<?cs
+    if:newticket.attachment ?> checked="checked"<?cs /if ?> />
+    I have files to attach to this ticket
+  </label>
+ </p><?cs
+ /if ?>
+
+ <div class="buttons">
+  <input type="submit" name="preview" value="Preview" accesskey="r" />&nbsp;
+  <input type="submit" value="Submit ticket" />
+ </div>
+</form>
+
+<div id="help">
+ <strong>Note:</strong> See <a href="<?cs
+   var:trac.href.wiki ?>/TracTickets">TracTickets</a> for help on using tickets.
+</div>
+</div>
+
+<?cs include "footer.cs" ?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/query.cs
@@ -0,0 +1,243 @@
+<?cs include:"header.cs" ?>
+<?cs include:"macros.cs" ?>
+
+<div id="ctxtnav" class="nav"><?cs
+ if:query.report_href ?><ul>
+  <li class="first"><a href="<?cs
+    var:query.report_href ?>">Available Reports</a></li>
+  <li class="last">Custom Query</li></ul><?cs
+ /if ?>
+</div>
+
+<?cs def:num_matches(v) ?><span class="numrows">(<?cs 
+ alt:v ?>No<?cs /alt ?> match<?cs if:v != 1 ?>es<?cs /if ?>)</span><?cs
+/def ?>
+
+<div id="content" class="query">
+ <h1><?cs var:title ?> <?cs call:num_matches(query.num_matches) ?></h1>
+
+<form id="query" method="post" action="<?cs var:trac.href.query ?>">
+ <fieldset id="filters">
+  <legend>Filters</legend>
+  <?cs def:checkbox_checked(constraint, option) ?><?cs
+   set:checked = 0 ?><?cs
+   each:value = constraint.values ?><?cs
+    if:(value == option) == (constraint.mode == '') ?><?cs
+      set:checked = 1 ?><?cs
+    /if ?><?cs
+   /each ?><?cs
+   if:checked ?> checked="checked"<?cs /if ?><?cs
+  /def ?>
+  <table summary="Query filters">
+   <tbody><tr style="height: 1px"><td colspan="4"></td></tr></tbody><?cs
+   each:field = query.fields ?><?cs
+   each:constraint = query.constraints ?><?cs
+    if:name(field) == name(constraint) ?>
+     <tbody><tr class="<?cs var:name(field) ?>">
+      <th scope="row"><label><?cs var:field.label ?></label></th><?cs
+      if:field.type != "radio" && field.type != "checkbox" ?>
+       <td class="mode">
+        <select name="<?cs var:name(field) ?>_mode"><?cs
+         each:mode = query.modes[field.type] ?>
+          <option value="<?cs var:mode.value ?>"<?cs
+           if:mode.value == constraint.mode ?> selected="selected"<?cs
+           /if ?>><?cs var:mode.name ?></option><?cs
+         /each ?>
+        </select>
+       </td><?cs
+      /if ?>
+      <td class="filter"<?cs
+        if:field.type == "radio" || field.type == "checkbox" ?> colspan="2"<?cs
+        /if ?>><?cs
+       if:field.type == "select" ?><?cs
+        each:value = constraint.values ?>
+         <select name="<?cs var:name(constraint) ?>"><option></option><?cs
+         each:option = field.options ?>
+          <option<?cs if:option == value ?> selected="selected"<?cs /if ?>><?cs
+            var:option ?></option><?cs
+         /each ?></select><?cs
+         if:name(value) != len(constraint.values) - 1 ?>
+          </td>
+          <td class="actions"><input type="submit" name="rm_filter_<?cs
+             var:name(field) ?>_<?cs var:name(value) ?>" value="-" /></td>
+         </tr><tr class="<?cs var:name(field) ?>">
+          <th colspan="2"><label>or</label></th>
+          <td class="filter"><?cs
+         /if ?><?cs
+        /each ?><?cs
+       elif:field.type == "radio" ?><?cs
+        each:option = field.options ?>
+         <input type="checkbox" id="<?cs var:name(field) ?>_<?cs
+           var:option ?>" name="<?cs var:name(field) ?>" value="<?cs
+           var:option ?>"<?cs call:checkbox_checked(constraint, option) ?> />
+         <label for="<?cs var:name(field) ?>_<?cs var:option ?>"><?cs
+           alt:option ?>none<?cs /alt ?></label><?cs
+        /each ?><?cs
+       elif:field.type == "checkbox" ?>
+        <input type="radio" id="<?cs var:name(field) ?>_on" name="<?cs
+          var:name(field) ?>" value="1"<?cs
+          if:constraint.mode != '!' ?> checked="checked"<?cs /if ?> />
+        <label for="<?cs var:name(field) ?>_on">yes</label>
+        <input type="radio" id="<?cs var:name(field) ?>_off" name="<?cs
+          var:name(field) ?>" value="!1"<?cs
+          if:constraint.mode == '!' ?> checked="checked"<?cs /if ?> />
+        <label for="<?cs var:name(field) ?>_off">no</label><?cs
+       elif:field.type == "text" ?><?cs
+        each:value = constraint.values ?>
+        <input type="text" name="<?cs var:name(field) ?>" value="<?cs
+          var:value ?>" size="42" /><?cs
+         if:name(value) != len(constraint.values) - 1 ?>
+          </td>
+          <td class="actions"><input type="submit" name="rm_filter_<?cs
+             var:name(field) ?>_<?cs var:name(value) ?>" value="-" /></td>
+         </tr><tr class="<?cs var:name(field) ?>">
+          <th colspan="2"><label>or</label></th>
+          <td class="filter"><?cs
+         /if ?><?cs
+        /each ?><?cs
+       /if ?>
+      </td>
+      <td class="actions"><input type="submit" name="rm_filter_<?cs
+         var:name(field) ?><?cs
+         if:field.type != 'radio' ?>_<?cs
+          var:len(constraint.values) - 1 ?><?cs
+         /if ?>" value="-" /></td>
+     </tr></tbody><?cs /if ?><?cs
+    /each ?><?cs
+   /each ?>
+   <tbody><tr class="actions">
+    <td class="actions" colspan="4" style="text-align: right">
+     <label for="add_filter">Add filter</label>&nbsp;
+     <select name="add_filter" id="add_filter">
+      <option></option><?cs
+      each:field = query.fields ?>
+       <option value="<?cs var:name(field) ?>"<?cs
+         if:field.type == "radio" ?><?cs
+          if:len(query.constraints[name(field)]) != 0 ?> disabled="disabled"<?cs
+          /if ?><?cs
+         /if ?>><?cs var:field.label ?></option><?cs
+      /each ?>	
+     </select>
+     <input type="submit" name="add" value="+" />
+    </td>
+   </tr></tbody>
+  </table>
+ </fieldset>
+ <p class="option">
+  <label for="group">Group results by</label>
+  <select name="group" id="group">
+   <option></option><?cs
+   each:field = query.fields ?><?cs
+    if:field.type == 'select' || field.type == 'radio' ||
+       name(field) == 'owner' ?>
+     <option value="<?cs var:name(field) ?>"<?cs
+       if:name(field) == query.group ?> selected="selected"<?cs /if ?>><?cs
+       var:field.label ?></option><?cs
+    /if ?><?cs
+   /each ?>
+  </select>
+  <input type="checkbox" name="groupdesc" id="groupdesc"<?cs
+    if:query.groupdesc ?> checked="checked"<?cs /if ?> />
+  <label for="groupdesc">descending</label>
+  <script type="text/javascript">
+    var group = document.getElementById("group");
+    var updateGroupDesc = function() {
+      enableControl('groupdesc', group.selectedIndex > 0);
+    }
+    addEvent(window, 'load', updateGroupDesc);
+    addEvent(group, 'change', updateGroupDesc);
+  </script>
+ </p>
+ <p class="option">
+  <input type="checkbox" name="verbose" id="verbose"<?cs
+    if:query.verbose ?> checked="checked"<?cs /if ?> />
+  <label for="verbose">Show full description under each result</label>
+ </p>
+ <div class="buttons">
+  <input type="hidden" name="order" value="<?cs var:query.order ?>" />
+  <?cs if:query.desc ?><input type="hidden" name="desc" value="1" /><?cs /if ?>
+  <input type="submit" name="update" value="Update" />
+ </div>
+ <hr />
+</form>
+<script type="text/javascript"><?cs set:idx = 0 ?>
+ var properties={<?cs each:field = query.fields ?><?cs
+  var:name(field) ?>:{type:"<?cs var:field.type ?>",label:"<?cs
+  var:field.label ?>",options:[<?cs
+   each:option = field.options ?>"<?cs var:option ?>"<?cs
+    if:name(option) < len(field.options) -1 ?>,<?cs /if ?><?cs
+   /each ?>]}<?cs
+  set:idx = idx + 1 ?><?cs if:idx < len(query.fields) ?>,<?cs /if ?><?cs
+ /each ?>};<?cs set:idx = 0 ?>
+ var modes = {<?cs each:type = query.modes ?><?cs var:name(type) ?>:[<?cs
+  each:mode = type ?>{text:"<?cs var:mode.name ?>",value:"<?cs var:mode.value ?>"}<?cs
+   if:name(mode) < len(type) -1 ?>,<?cs /if ?><?cs
+  /each ?>]<?cs
+  set:idx = idx + 1 ?><?cs if:idx < len(query.modes) ?>,<?cs /if ?><?cs
+ /each ?>};
+ initializeFilters();
+</script>
+
+<?cs def:thead() ?>
+ <thead><tr><?cs each:header = query.headers ?>
+  <th class="<?cs var:header.name ?><?cs if:query.order == header.name ?> <?cs
+    if:query.desc ?>desc<?cs else ?>asc<?cs /if ?><?cs /if ?>">
+   <a title="Sort by <?cs var:header.label ?><?cs
+     if:query.order == header.name && !query.desc ?> (descending)<?cs
+     /if ?>" href="<?cs var:header.href ?>"><?cs var:header.label ?></a>
+  </th><?cs
+ /each ?></tr></thead>
+<?cs /def ?>
+
+<?cs if:len(query.results) ?><?cs
+ if:!query.group ?>
+  <table class="listing tickets">
+  <?cs call:thead() ?><tbody><?cs
+ /if ?><?cs
+ each:result = query.results ?><?cs
+  if:result[query.group] != prev_group ?>
+   <?cs if:prev_group ?></tbody></table><?cs /if ?>
+   <h2><?cs
+    each:field = query.fields ?><?cs
+     if:name(field) == query.group ?><?cs
+      var:field.label ?><?cs
+     /if ?><?cs
+    /each ?>: <?cs var:result[query.group] ?> <?cs call:num_matches(query.num_matches_group[result[query.group]]) ?></h2>
+   <table class="listing tickets">
+   <?cs call:thead() ?><tbody><?cs
+  /if ?>
+  <tr class="<?cs
+   if:name(result) % 2 ?>odd<?cs else ?>even<?cs /if ?> prio<?cs
+   var:result.priority_value ?><?cs
+   if:result.added ?> added<?cs /if ?><?cs
+   if:result.changed ?> changed<?cs /if ?><?cs
+   if:result.removed ?> removed<?cs /if ?>"><?cs
+  each:header = query.headers ?><?cs
+   if:name(header) == 0 ?><td class="id"><a href="<?cs
+    var:result.href ?>" title="View ticket"><?cs var:result.id ?></a></td><?cs
+   else ?><td class="<?cs var:header.name ?>"><?cs
+     if:header.name == 'summary' ?><a href="<?cs
+      var:result.href ?>" title="View ticket"><?cs
+      var:result.summary ?></a><?cs
+     else ?><span><?cs var:result[header.name] ?></span><?cs
+     /if ?></td><?cs
+   /if ?><?cs
+  /each ?>
+  <?cs if:query.verbose ?>
+   </tr><tr class="fullrow"><td colspan="<?cs var:len(query.headers) ?>">
+    <p class="meta">Reported by <strong><?cs var:result.reporter ?></strong>,
+    <?cs var:result.time ?><?cs if:result.description ?>:<?cs /if ?></p>
+    <?cs if:result.description ?><p><?cs var:result.description ?></p><?cs /if ?>
+   </td>
+  <?cs /if ?><?cs set:prev_group = result[query.group] ?>
+ </tr><?cs /each ?>
+</tbody></table><?cs
+/if ?>
+
+<div id="help">
+ <strong>Note:</strong> See <a href="<?cs var:trac.href.wiki ?>/TracQuery">TracQuery</a> 
+ for help on using queries.
+</div>
+
+</div>
+<?cs include:"footer.cs" ?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/query_rss.cs
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<rss version="2.0">
+ <channel><?cs
+  if:project.name_encoded ?>
+   <title><?cs var:project.name_encoded ?>: Ticket Query</title><?cs
+  else ?>
+   <title>Ticket Query</title><?cs
+  /if ?>
+  <link><?cs var:query.href ?></link><?cs
+  if:project.descr ?>
+   <description><?cs var:project.descr ?></description><?cs
+  /if ?>
+  <language>en-us</language>
+  <image>
+   <title><?cs var:project.name_encoded ?></title>
+   <url><?cs
+    if:!header_logo.src_abs ?><?cs var:base_host ?><?cs
+    /if ?><?cs
+    var:header_logo.src ?></url>
+   <link><?cs var:base_host ?><?cs var:trac.href.timeline ?></link><?cs
+   if:header_logo.width ?>
+    <width><?cs var:header_logo.width ?></width><?cs
+   /if ?><?cs
+   if:header_logo.height ?>
+    <height><?cs var:header_logo.height ?></height><?cs
+   /if ?>
+  </image>
+  <generator>Trac v<?cs var:trac.version ?></generator><?cs
+  each:result = query.results ?>
+   <item>
+    <link><?cs var:result.href ?></link>
+    <guid isPermaLink="true"><?cs var:result.href ?></guid>
+    <title><?cs var:'#' + result.id + ': ' + result.summary ?></title><?cs
+    if:result.created ?>
+     <pubDate><?cs var:result.created ?></pubDate><?cs
+    /if ?><?cs
+    if:result.reporter ?>
+     <author><?cs var:result.reporter ?></author><?cs
+    /if ?>
+    <description><?cs var:result.description ?></description>
+    <category>Tickets</category>
+    <comments><?cs var:result.href ?>#changelog</comments>
+   </item><?cs
+  /each ?>
+ </channel>
+</rss>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/report.cs
@@ -0,0 +1,244 @@
+<?cs include "header.cs" ?>
+<?cs include "macros.cs" ?>
+
+<div id="ctxtnav" class="nav">
+ <h2>Report Navigation</h2>
+ <ul><li class="first"><?cs
+   if:chrome.links.up.0.href ?><a href="<?cs
+    var:chrome.links.up.0.href ?>">Available Reports</a><?cs
+   else ?>Available Reports<?cs
+  /if ?></li><?cs
+  if:report.query_href ?><li class="last"><a href="<?cs
+   var:report.query_href ?>">Custom Query</a></li><?cs
+  /if ?></ul>
+</div>
+
+<div id="content" class="report">
+
+ <?cs def:report_hdr(header) ?>
+   <?cs if $header ?>
+     <?cs if idx > 0 ?>
+       </table>
+     <?cs /if ?>
+   <?cs /if ?>
+   <?cs if:header ?><h2><?cs var:header ?></h2><?cs /if ?>
+   <?cs if $report.id == -1 ?>
+     <table class="listing reports">
+   <?cs else ?>
+     <table class="listing tickets">
+   <?cs /if ?>
+    <thead>
+     <tr>
+       <?cs set numcols = #0 ?>
+       <?cs each header = report.headers ?>
+         <?cs if $header.fullrow ?>
+           </tr><tr><th colspan="100"><?cs var:header ?></th>
+         <?cs else ?>
+           <?cs if $report.sorting.enabled ?>
+             <?cs set vars='' ?>
+             <?cs each arg = report.var ?>
+               <?cs set vars = vars + '&amp;' + name(arg) + '=' + arg ?>
+             <?cs /each ?>
+             <?cs set sortValue = '' ?>
+             <?cs if $header.asc == '1' ?>
+               <?cs set sortValue = '?sort='+$header.real+'&amp;asc=0' ?>
+             <?cs else ?>
+               <?cs set sortValue = '?sort='+$header.real+'&amp;asc=1' ?>
+             <?cs /if ?>
+             <?cs if $header ?>
+             <th><a href="<?cs var:sortValue ?><?cs var:vars ?>"><?cs var:header ?></a></th>
+             <?cs /if ?>
+           <?cs elif $header ?>
+             <th><?cs var:header ?></th>
+           <?cs /if ?>
+           <?cs if $header.breakrow ?>
+              </tr><tr>
+           <?cs /if ?>
+         <?cs /if ?>
+         <?cs set numcols = numcols + #1 ?>
+       <?cs /each ?>
+     </tr>
+    </thead>
+ <?cs /def ?>
+ 
+ <?cs def:report_cell(class,contents) ?>
+   <?cs if $cell.fullrow ?>
+     </tr><tr class="<?cs var:row_class ?>" style="<?cs var: row_style ?>;border: none; padding: 0;">
+      <td class="fullrow" colspan="100">
+       <?cs var:$contents ?><hr />
+      </td>
+   <?cs else ?>
+   <td <?cs if $cell.breakrow || $col == $numcols ?>colspan="100" <?cs /if
+ ?>class="<?cs var:$class ?>"><?cs if $contents ?><?cs var:$contents ?><?cs /if ?></td>
+ 
+ <?cs if $cell.breakafter ?>
+     </tr><tr class="<?cs var: row_class ?>" style="<?cs var: row_style ?>;border: none; padding: 0">
+ <?cs /if ?>
+   <?cs /if ?>
+   <?cs set col = $col + #1 ?>
+ <?cs /def ?>
+ 
+ <?cs set idx = #0 ?>
+ <?cs set group = '' ?>
+ 
+ <?cs if:report.mode == "list" ?>
+  <h1><?cs var:title ?><?cs
+   if:report.numrows && report.id != -1 ?><span class="numrows"> (<?cs
+    var:report.numrows ?> matches)</span><?cs
+   /if ?></h1><?cs
+   if:report.description ?><div id="description"><?cs
+    var:report.description ?></div><?cs
+   /if ?><?cs
+   if:report.id != -1 ?><?cs
+    if:report.can_create || report.can_modify || report.can_delete ?>
+     <div class="buttons"><?cs
+      if:report.can_modify ?><form action="" method="get"><div>
+       <input type="hidden" name="action" value="edit" />
+       <input type="submit" value="Edit report" accesskey="e" />
+      </div></form><?cs /if ?><?cs
+      if:report.can_create ?><form action="" method="get"><div>
+       <input type="hidden" name="action" value="copy" />
+       <input type="submit" value="Copy report" />
+      </div></form><?cs /if ?><?cs
+      if:report.can_delete ?><form action="" method="get"><div>
+       <input type="hidden" name="action" value="delete" />
+       <input type="submit" value="Delete report" />
+      </div></form><?cs /if ?>
+     </div><?cs
+    /if ?><?cs
+   /if ?>
+
+     <?cs each row = report.items ?>
+       <?cs if group != row.__group__ || idx == #0 ?>
+         <?cs if:idx != #0 ?></tbody><?cs /if ?>
+         <?cs set group = row.__group__ ?>
+         <?cs call:report_hdr(group) ?>
+         <tbody>
+       <?cs /if ?>
+
+       <?cs if row.__color__ ?>
+         <?cs set rstem='color'+$row.__color__ +'-' ?>
+       <?cs else ?>
+        <?cs set rstem='' ?>
+       <?cs /if ?>
+       <?cs if idx % #2 ?>
+         <?cs set row_class=$rstem+'even' ?>
+       <?cs else ?>
+         <?cs set row_class=$rstem+'odd' ?>
+       <?cs /if ?>
+
+       <?cs set row_style='' ?>
+       <?cs if row.__bgcolor__ ?>
+         <?cs set row_style='background: ' + row.__bgcolor__ + ';' ?>
+       <?cs /if ?>
+       <?cs if row.__fgcolor__ ?>
+         <?cs set row_style=$row_style + 'color: ' + row.__fgcolor__ + ';' ?>
+       <?cs /if ?>
+       <?cs if row.__style__ ?>
+         <?cs set row_style=$row_style + row.__style__ + ';' ?>
+       <?cs /if ?>
+
+       <tr class="<?cs var: row_class ?>" style="<?cs var: row_style ?>">
+       <?cs set idx = idx + #1 ?>
+       <?cs set col = #0 ?>
+       <?cs each cell = row ?>
+         <?cs if cell.hidden || cell.hidehtml ?>
+         <?cs elif name(cell) == "ticket" || name(cell) == "id" ?>
+           <?cs call:report_cell('ticket',
+                                 '<a title="View ticket" href="'+
+                                 $cell.ticket_href+'">#'+$cell+'</a>') ?>
+         <?cs elif name(cell) == "summary" && cell.ticket_href ?>
+           <?cs call:report_cell('summary', '<a title="View ticket" href="'+
+                                 $cell.ticket_href+'">'+$cell+'</a>') ?>
+         <?cs elif name(cell) == "report" ?>
+           <?cs call:report_cell('report',
+                '<a title="View report" href="'+$cell.report_href+'">{'+$cell+'}</a>') ?>
+           <?cs set:report_href=$cell.report_href ?>
+         <?cs elif name(cell) == "time" ?>
+           <?cs call:report_cell('date', $cell.date) ?>
+         <?cs elif name(cell) == "date" || name(cell) == "created" || name(cell) == "modified" ?>
+           <?cs call:report_cell('date', $cell.date) ?>
+         <?cs elif name(cell) == "datetime"  ?>
+           <?cs call:report_cell('date', $cell.datetime) ?>
+         <?cs elif name(cell) == "description" ?>
+           <?cs call:report_cell('', $cell.parsed) ?>
+         <?cs elif name(cell) == "title" && $report.id == -1 ?>
+           <?cs call:report_cell('title',
+                                 '<a  title="View report" href="'+
+                                 $report_href+'">'+$cell+'</a>') ?>
+         <?cs else ?>
+           <?cs call:report_cell(name(cell), $cell) ?>
+         <?cs /if ?>
+       <?cs /each ?>
+       </tr>
+     <?cs /each ?>
+    </tbody>
+   </table><?cs
+   if:report.id == -1 && report.can_create?><div class="buttons">
+    <form action="" method="get"><div>
+     <input type="hidden" name="action" value="new" />
+     <input type="submit" value="Create new report" />
+    </div></form></div><?cs
+   /if ?><?cs
+   if report.message ?>
+    <div class="system-message"><?cs var report.message ?></div><?cs
+   elif:idx == #0 ?>
+    <div id="report-notfound">No matches found.</div><?cs
+   /if ?>
+
+ <?cs elif:report.mode == "delete" ?>
+
+  <h1><?cs var:title ?></h1>
+  <form action="<?cs var:report.href ?>" method="post">
+   <input type="hidden" name="id" value="<?cs var:report.id ?>" />
+   <input type="hidden" name="action" value="delete" />
+   <p><strong>Are you sure you want to delete this report?</strong></p>
+   <div class="buttons">
+    <input type="submit" name="cancel" value="Cancel" />
+    <input type="submit" value="Delete report" />
+   </div>
+  </form>
+ 
+ <?cs elif:report.mode == "edit" ?>
+ 
+   <h1><?cs var:title ?></h1>
+   <form action="<?cs var:report.href ?>" method="post">
+    <div>
+     <input type="hidden" name="action" value="<?cs var:report.action ?>" />
+     <div class="field">
+      <label for="title">Report Title:</label><br />
+      <input type="text" id="title" name="title"
+             value="<?cs var:report.title ?>" size="50" /><br />
+     </div>
+     <div class="field">
+      <label for="description">
+       Description:</label> (You may use <a tabindex="42" href="<?cs
+         var:$trac.href.wiki ?>/WikiFormatting">WikiFormatting</a> here)
+      </label><br />
+      <textarea id="description" name="description" class="wikitext" rows="10" cols="78">
+<?cs var:report.description ?></textarea>
+     </div>
+     <div class="field">
+      <label for="query">
+       SQL Query for Report:</label><br />
+      <textarea id="query" name="query" cols="85" rows="20"><?cs
+        var:report.sql ?></textarea>
+     </div>
+     <div class="buttons">
+      <input type="submit" value="Save report" />
+      <input type="submit" name="cancel" value="Cancel" />
+     </div>
+    </div>
+    <script type="text/javascript" src="<?cs
+      var:htdocs_location ?>js/wikitoolbar.js"></script>
+   </form>
+ <?cs /if?>
+ 
+ <div id="help">
+  <strong>Note:</strong> See <a href="<?cs
+    var:trac.href.wiki ?>/TracReports">TracReports</a> for help on using and
+  creating reports.
+ </div>
+ 
+</div>
+<?cs include "footer.cs" ?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/report_rss.cs
@@ -0,0 +1,42 @@
+<?xml version="1.0"?>
+<rss version="2.0">
+ <channel><?cs
+ if:project.name_encoded ?>
+  <title><?cs var:project.name_encoded ?>: <?cs var:report.title ?></title><?cs
+ else ?>
+  <title><?cs var:title ?></title><?cs
+ /if ?>
+ <link><?cs var:base_host ?><?cs var:trac.href.report ?>/<?cs var:report.id ?></link>
+ <description>Trac Report - <?cs var:report.title ?></description>
+ <language>en-us</language>
+ <generator>Trac v<?cs var:trac.version ?></generator><?cs
+ each:row = report.items ?><?cs
+  set title = '' ?><?cs
+  set descr = '' ?><?cs
+  set author = '' ?><?cs
+  set pubdate = '' ?><?cs
+  each:item = row ?><?cs
+   if name(item) == 'ticket' ?><?cs
+    set:link = base_host + item.ticket_href ?><?cs
+    set:id = item ?><?cs
+   elif:name(item) == 'summary' ?><?cs
+    set:title = item ?><?cs
+   elif:name(item) == 'description' ?><?cs
+    set:descr = item.parsed ?><?cs
+   elif:name(item) == 'reporter' ?><?cs
+    set:author = item.rss ?><?cs
+   elif:name(item) == 'time' || name(item) == 'changetime'
+     || name(item) == 'created' || name(item) == 'modified' ?><?cs
+    set pubdate = item.gmt ?><?cs
+   /if ?><?cs
+  /each ?>
+  <item>
+   <?cs if:author ?><author><?cs var:author ?></author><?cs /if ?>
+   <pubDate><?cs var:pubdate ?></pubDate>
+   <title><?cs var:'#' + id + ': ' + title ?></title>   
+   <link><?cs var:link ?></link>
+   <description><?cs var:descr ?></description>
+   <category>Report</category>
+  </item><?cs
+ /each ?></channel>
+</rss>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/roadmap.cs
@@ -0,0 +1,86 @@
+<?cs include "header.cs"?>
+<?cs include "macros.cs"?>
+
+<div id="ctxtnav" class="nav"></div>
+
+<div id="content" class="roadmap">
+ <h1>Roadmap</h1>
+
+ <form id="prefs" method="get" action="">
+  <div>
+   <input type="checkbox" id="showall" name="show" value="all"<?cs
+    if:roadmap.showall ?> checked="checked"<?cs /if ?> />
+   <label for="showall">Show already completed milestones</label>
+  </div>
+  <div class="buttons">
+   <input type="submit" value="Update" />
+  </div>
+ </form>
+
+ <ul class="milestones"><?cs each:milestone = roadmap.milestones ?>
+  <li class="milestone">
+   <div class="info">
+    <h2><a href="<?cs var:milestone.href ?>">Milestone: <em><?cs
+      var:milestone.name ?></em></a></h2>
+    <p class="date"<?cs
+     if:milestone.completed_date ?> title="<?cs var:milestone.completed_date ?>">
+      Completed <?cs var:milestone.completed_delta ?> ago<?cs
+     elif:milestone.due_date ?> title="<?cs var:milestone.due_date ?>"><?cs
+      if:milestone.late ?>
+       <strong><?cs var:milestone.due_delta ?> late</strong><?cs
+      else ?>
+       Due in <?cs var:milestone.due_delta ?><?cs
+      /if ?> (<?cs var:milestone.due_date ?>)<?cs
+     else ?>>
+      No date set<?cs
+     /if ?>
+    </p><?cs
+    with:stats = milestone.stats ?><?cs
+     if:#stats.total_tickets > #0 ?>
+      <table class="progress">
+       <tr>
+        <td class="closed" style="width: <?cs
+          var:#stats.percent_closed ?>%"><a href="<?cs
+          var:milestone.queries.closed_tickets ?>" title="<?cs
+          var:#stats.closed_tickets ?> of <?cs
+          var:#stats.total_tickets ?> ticket<?cs
+          if:#stats.total_tickets != #1 ?>s<?cs /if ?> closed"></a></td>
+        <td class="open" style="width: <?cs
+          var:#stats.percent_active ?>%"><a href="<?cs
+          var:milestone.queries.active_tickets ?>" title="<?cs
+          var:#stats.active_tickets ?> of <?cs
+          var:#stats.total_tickets ?> ticket<?cs
+          if:#stats.total_tickets != #1 ?>s<?cs /if ?> active"></a></td>
+       </tr>
+      </table>
+      <p class="percent"><?cs var:#stats.percent_closed ?>%</p>
+      <dl>
+       <dt>Closed tickets:</dt>
+       <dd><a href="<?cs var:milestone.queries.closed_tickets ?>"><?cs
+         var:stats.closed_tickets ?></a></dd>
+       <dt>Active tickets:</dt>
+       <dd><a href="<?cs var:milestone.queries.active_tickets ?>"><?cs
+         var:stats.active_tickets ?></a></dd>
+      </dl><?cs
+     /if ?><?cs
+    /with ?>
+   </div>
+   <div class="description"><?cs var:milestone.description ?></div>
+  </li><?cs
+ /each ?></ul><?cs
+ if:trac.acl.MILESTONE_CREATE ?>
+  <div class="buttons">
+   <form method="get" action="<?cs var:trac.href.milestone ?>"><div>
+    <input type="hidden" name="action" value="new" />
+    <input type="submit" value="Add new milestone" />
+   </div></form>
+  </div><?cs
+ /if ?>
+
+ <div id="help">
+  <strong>Note:</strong> See <a href="<?cs
+    var:trac.href.wiki ?>/TracRoadmap">TracRoadmap</a> for help on using the roadmap.
+ </div>
+
+</div>
+<?cs include:"footer.cs"?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/search.cs
@@ -0,0 +1,102 @@
+<?cs include:"header.cs"?>
+<script type="text/javascript">
+addEvent(window, 'load', function() { document.getElementById('q').focus()}); 
+</script>
+<div id="ctxtnav" class="nav"><?cs
+ with:links = chrome.links ?><?cs
+  if:len(links.prev) || len(links.next) ?><ul><?cs
+   if:len(links.prev) ?>
+    <li class="first<?cs if:!len(links.up) && !len(links.next) ?> last<?cs /if ?>">
+     &larr; <a href="<?cs var:links.prev.0.href ?>"><?cs
+       var:links.prev.0.title ?></a>
+    </li><?cs
+   /if ?><?cs
+   if:len(links.next) ?>
+    <li class="<?cs if:!len(links.prev) && !len(links.up) ?>first <?cs /if ?>last">
+     <a href="<?cs var:links.next.0.href ?>"><?cs
+       var:links.next.0.title ?></a> &rarr;
+    </li><?cs
+   /if ?></ul><?cs
+  /if ?><?cs
+ /with ?>
+</div>
+
+<div id="content" class="search">
+
+<h1><label for="q">Search</label></h1>
+<form action="<?cs var:trac.href.search ?>" method="get">
+ <p>
+  <input type="text" id="q" name="q" size="40" value="<?cs var:search.q ?>" />
+  <input type="hidden" name="noquickjump" value="1" />
+  <input type="submit" value="Search" />
+ </p>
+ <p><?cs
+  each filter=search.filters ?>
+   <input type="checkbox" id="<?cs var:filter.name?>" 
+          name="<?cs var:filter.name?>" <?cs
+     if:filter.active ?>checked="checked"<?cs /if ?> />
+   <label for="<?cs var:filter.name ?>"><?cs var:filter.label?></label><?cs
+  /each ?>
+ </p>
+</form><?cs 
+
+if:len(search.result) || len(search.quickjump) ?>
+ <hr /><?cs
+ if:len(search.result) ?>
+ <h2>Search results <?cs
+  if:search.n_pages > 1 ?>(<?cs
+   var:(search.page-1) * search.page_size + 1 ?> - <?cs
+   var:(search.page-1) * search.page_size + len(search.result) ?> 
+   of <?cs var:search.n_hits?>)<?cs
+  /if ?></h2><?cs
+ /if ?>
+ <div id="searchable">
+  <dl id="results"><?cs
+   if:len(search.quickjump) ?>
+    <dt id=quickjump><a href="<?cs var:search.quickjump.href ?>">Quickjump to <?cs var:search.quickjump.name ?></a></dt>
+    <dd><?cs var:search.quickjump.description ?></dd><?cs 
+   /if ?><?cs 
+   each item=search.result ?>
+    <dt><a href="<?cs var:item.href ?>"><?cs var:item.title ?></a></dt>
+    <dd><?cs var:item.excerpt ?></dd>
+    <dd>
+     <span class="author">By <?cs var:item.author ?></span> &mdash;
+     <span class="date"><?cs var:item.date ?></span><?cs
+     if:item.keywords ?> &mdash
+      <span class="keywords">Keywords: <em><?cs var:item.keywords ?></em></span><?cs
+     /if ?>
+    </dd><?cs
+   /each ?>
+  </dl>
+  <hr />
+ </div><?cs 
+ if search.n_pages > 1 ?>
+  <div id="paging"><?cs
+  if len(chrome.links.prev) ?>
+    <a href="<?cs var:chrome.links.prev.0.href ?>" title="<?cs
+       var:chrome.links.prev.0.title ?>">&larr;</a> <?cs
+  /if ?><?cs
+  loop:p = 1, search.n_pages ?><?cs
+    if p == search.page ?><?cs var:p ?><?cs
+    else ?><a href="<?cs var:search.page_href + "&amp;page=" + p?>"><?cs
+     var:p ?></a><?cs
+    /if ?> <?cs
+  /loop ?><?cs
+  if len(chrome.links.next) ?>
+    <a href="<?cs var:chrome.links.next.0.href ?>" title="<?cs
+       var:chrome.links.next.0.title ?>">&rarr;</a><?cs
+  /if ?>
+  </div><?cs
+ /if ?><?cs
+
+elif:search.q && !search.quickjump ?>
+ <div id="notfound">No matches found.</div><?cs
+/if ?>
+
+<div id="help">
+ <strong>Note:</strong> See <a href="<?cs
+   var:trac.href.wiki ?>/TracSearch">TracSearch</a>  for help on searching.
+</div>
+
+</div>
+<?cs include:"footer.cs"?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/settings.cs
@@ -0,0 +1,66 @@
+<?cs include "header.cs"?>
+<?cs include "macros.cs"?>
+
+<div id="ctxtnav" class="nav"></div>
+
+<div id="content" class="settings">
+
+ <h1>Settings and Session Management</h1>
+
+ <h2>User Settings</h2>
+ <p>
+ This page lets you customize and personalize your Trac settings. Session
+ settings are stored on the server and identified using  a 'Session Key'
+ stored in a browser cookie. The cookie lets Trac restore your settings.
+ </p>
+ <form method="post" action="">
+ <div>
+  <h3>Personal Information</h3>
+  <div>
+   <input type="hidden" name="action" value="save" />
+   <label for="name">Name:</label>
+   <input type="text" id="name" name="name" class="textwidget" size="30"
+          value="<?cs var:settings.name ?>" />
+  </div>
+  <div>
+   <label for="email">Email:</label>
+   <input type="text" id="email" name="email" class="textwidget" size="30"
+          value="<?cs var:settings.email ?>" />
+  </div><?cs
+  if:settings.session_id ?>
+   <h3>Session</h3>
+   <div>
+    <label for="newsid">Session Key:</label>
+    <input type="text" id="newsid" name="newsid" class="textwidget" size="30"
+           value="<?cs var:settings.session_id ?>" />
+    <p>The session key is used to identify stored  custom settings and session
+    data on the server. Automatically generated by default, you may change it
+    to something easier to remember at any time if you wish to use your settings
+    in a different web browser.</p>
+   </div><?cs
+  /if ?>
+  <div>
+   <br />
+   <input type="submit" value="Submit changes" />
+  </div >
+ </div>
+</form><?cs
+if:settings.session_id ?>
+ <hr />
+ <h2>Load Session</h2>
+ <p>You may load a previously created session by entering the corresponding
+ session key below and clicking 'Recover'. This lets you share settings between
+ multiple computers and/or web browsers.</p>
+ <form method="post" action="">
+  <div>
+   <input type="hidden" name="action" value="load" />
+   <label for="loadsid">Existing Session Key:</label>
+   <input type="text" id="loadsid" name="loadsid" class="textwidget" size="30"
+          value="" />
+   <input type="submit" value="Recover" />
+  </div>
+ </form><?cs
+/if ?>
+
+</div>
+<?cs include:"footer.cs"?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/ticket.cs
@@ -0,0 +1,327 @@
+<?cs include "header.cs" ?>
+<?cs include "macros.cs" ?>
+
+<div id="ctxtnav" class="nav">
+ <h2>Ticket Navigation</h2><?cs
+ with:links = chrome.links ?><?cs
+  if:len(links.prev) || len(links.up) || len(links.next) ?><ul><?cs
+   if:len(links.prev) ?>
+    <li class="first<?cs if:!len(links.up) && !len(links.next) ?> last<?cs /if ?>">
+     &larr; <a href="<?cs var:links.prev.0.href ?>" title="<?cs
+       var:links.prev.0.title ?>">Previous Ticket</a>
+    </li><?cs
+   /if ?><?cs
+   if:len(links.up) ?>
+    <li class="<?cs if:!len(links.prev) ?>first<?cs /if ?><?cs
+                    if:!len(links.next) ?> last<?cs /if ?>">
+     <a href="<?cs var:links.up.0.href ?>" title="<?cs
+       var:links.up.0.title ?>">Back to Query</a>
+    </li><?cs
+   /if ?><?cs
+   if:len(links.next) ?>
+    <li class="<?cs if:!len(links.prev) && !len(links.up) ?>first <?cs /if ?>last">
+     <a href="<?cs var:links.next.0.href ?>" title="<?cs
+       var:links.next.0.title ?>">Next Ticket</a> &rarr;
+    </li><?cs
+   /if ?></ul><?cs
+  /if ?><?cs
+ /with ?>
+</div>
+
+<div id="content" class="ticket">
+
+ <h1>Ticket #<?cs var:ticket.id ?> <span class="status">(<?cs 
+  var:ticket.status ?><?cs 
+  if:ticket.type ?> <?cs var:ticket.type ?><?cs 
+  /if ?><?cs 
+  if:ticket.resolution ?>: <?cs var:ticket.resolution ?><?cs 
+  /if ?>)</span></h1>
+
+<div id="searchable">
+<div id="ticket">
+ <div class="date">
+  <p title="<?cs var:ticket.opened ?>">Opened <?cs var:ticket.opened_delta ?> ago</p><?cs
+  if:ticket.lastmod ?>
+   <p title="<?cs var:ticket.lastmod ?>">Last modified <?cs var:ticket.lastmod_delta ?> ago</p>
+  <?cs /if ?>
+ </div>
+ <h2 class="summary"><?cs var:ticket.summary ?></h2>
+ <table class="properties">
+  <tr>
+   <th id="h_reporter">Reported by:</th>
+   <td headers="h_reporter"><?cs var:ticket.reporter ?></td>
+   <th id="h_owner">Assigned to:</th>
+   <td headers="h_owner"><?cs var:ticket.owner ?><?cs
+     if:ticket.status == 'assigned' ?> (accepted)<?cs /if ?></td>
+  </tr><tr><?cs
+  each:field = ticket.fields ?><?cs
+   if:!field.skip ?><?cs
+    set:num_fields = num_fields + 1 ?><?cs
+   /if ?><?cs
+  /each ?><?cs
+  set:idx = 0 ?><?cs
+  each:field = ticket.fields ?><?cs
+   if:!field.skip ?><?cs set:fullrow = field.type == 'textarea' ?><?cs
+    if:fullrow && idx % 2 ?><th></th><td></td></tr><tr><?cs /if ?>
+    <th id="h_<?cs var:name(field) ?>"><?cs var:field.label ?>:</th>
+    <td<?cs if:fullrow ?> colspan="3"<?cs /if ?> headers="h_<?cs
+      var:name(field) ?>"><?cs var:ticket[name(field)] ?></td><?cs 
+    if:idx % 2 || fullrow ?></tr><tr><?cs 
+    elif:idx == num_fields - 1 ?><th></th><td></td><?cs
+    /if ?><?cs set:idx = idx + #fullrow + 1 ?><?cs
+   /if ?><?cs
+  /each ?></tr>
+ </table>
+ <?cs if:ticket.description ?><div id="comment:description" class="description">
+  <?cs var:ticket.description.formatted ?>
+  <form method="get" action="<?cs var:ticket.href ?>#comment"><div class="inlinebuttons">
+   <input type="hidden" name="replyto" value="description" />
+   <input type="submit" value="Reply" title="Reply, quoting this description" /></div>
+  </form>
+ </div><?cs /if ?>
+</div>
+
+<?cs if:ticket.attach_href || len(ticket.attachments) ?>
+<?cs call:list_of_attachments(ticket.attachments, ticket.attach_href) ?>
+<?cs /if ?>
+
+<?cs def:commentref(prefix, cnum) ?>
+<a href="#comment:<?cs var:cnum ?>"><small><?cs var:prefix ?><?cs var:cnum ?></small></a>
+<?cs /def ?>
+
+<?cs if:len(ticket.changes) ?><h2>Change History</h2>
+<div id="changelog"><?cs
+ each:change = ticket.changes ?>
+ <div class="change">
+  <h3 <?cs if:change.cnum ?>id="comment:<?cs var:change.cnum ?>"<?cs /if ?>><?cs
+   if:change.cnum ?>
+    <span class="threading"><?cs
+     set:nreplies = len(ticket.replies[change.cnum]) ?><?cs
+     if:nreplies || change.replyto ?>(<?cs
+      if:change.replyto ?>in reply to: <?cs 
+       call:commentref('&uarr;&nbsp;', change.replyto) ?><?cs if nreplies ?>; <?cs /if ?><?cs
+      /if ?><?cs
+      if nreplies ?><?cs
+       call:plural('follow-up', nreplies) ?>: <?cs 
+       each:reply = ticket.replies[change.cnum] ?><?cs 
+        call:commentref('&darr;&nbsp;', reply) ?><?cs 
+       /each ?><?cs 
+      /if ?>)<?cs
+    /if ?><form method="get" action="<?cs var:ticket.href ?>#comment"><span class="inlinebuttons">
+    <input type="hidden" name="replyto" value="<?cs var:change.cnum ?>" />
+    <input type="submit" value="Reply" title="Reply to comment <?cs var:change.cnum ?>" /></span>
+   </form>
+    </span><?cs
+   /if ?><?cs
+   var:change.date ?> changed by <?cs var:change.author ?><?cs
+   if:change.cnum ?>&nbsp;<a href="#comment:<?cs var:change.cnum ?>" class="anchor"
+      title="Permalink to comment:<?cs var:change.cnum ?>">&para;</a><?cs
+   /if ?>
+  </h3><?cs
+  if:len(change.fields) ?>
+   <ul class="changes"><?cs
+   each:field = change.fields ?>
+    <li><strong><?cs var:name(field) ?></strong> <?cs
+    if:name(field) == 'attachment' ?><em><?cs var:field.new ?></em> added<?cs
+    elif:field.old && field.new ?>changed from <em><?cs
+     var:field.old ?></em> to <em><?cs var:field.new ?></em><?cs
+    elif:!field.old && field.new ?>set to <em><?cs var:field.new ?></em><?cs
+    elif:field.old && !field.new ?>deleted<?cs
+    else ?>changed<?cs
+    /if ?>.</li>
+    <?cs
+   /each ?>
+   </ul><?cs
+  /if ?>
+  <div class="comment"><?cs var:change.comment ?></div>
+ </div><?cs
+ /each ?>
+</div><?cs
+/if ?>
+
+<?cs if:trac.acl.TICKET_CHGPROP || trac.acl.TICKET_APPEND ?>
+<form action="<?cs var:ticket.href ?>#preview" method="post">
+ <hr />
+ <h3><a name="edit" onfocus="document.getElementById('comment').focus()">Add/Change #<?cs
+   var:ticket.id ?> (<?cs var:ticket.summary ?>)</a></h3>
+ <?cs if:trac.authname == "anonymous" ?>
+  <div class="field">
+   <label for="author">Your email or username:</label><br />
+   <input type="text" id="author" name="author" size="40"
+     value="<?cs var:ticket.reporter_id ?>" /><br />
+  </div>
+ <?cs /if ?>
+ <div class="field">
+  <fieldset class="iefix">
+   <label for="comment">Comment (you may use <a tabindex="42" href="<?cs
+     var:trac.href.wiki ?>/WikiFormatting">WikiFormatting</a> here):</label><br />
+   <p><textarea id="comment" name="comment" class="wikitext" rows="10" cols="78">
+<?cs var:ticket.comment ?></textarea></p>
+  </fieldset><?cs
+  if ticket.comment_preview ?>
+   <fieldset id="preview">
+    <legend>Comment Preview</legend>
+    <?cs var:ticket.comment_preview ?>
+   </fieldset><?cs
+  /if ?>
+ </div>
+
+ <?cs if:trac.acl.TICKET_CHGPROP ?><fieldset id="properties">
+  <legend>Change Properties</legend>
+  <table><tr>
+   <th><label for="summary">Summary:</label></th>
+   <td class="fullrow" colspan="3"><input type="text" id="summary" name="summary" value="<?cs
+     var:ticket.summary ?>" size="70" /></td>
+   </tr><?cs
+   if:len(ticket.fields.type.options) ?>
+   <tr>
+    <th><label for="type">Type:</label></th>
+    <td><?cs 
+     call:hdf_select(ticket.fields.type.options, 'type', ticket.type, 0) ?>
+    </td>
+   </tr><?cs
+   /if ?><?cs
+   if:trac.acl.TICKET_ADMIN ?><tr>
+    <th><label for="description">Description:</label></th>
+    <td class="fullrow" colspan="3">
+     <textarea id="description" name="description" class="wikitext" rows="10" cols="68">
+<?cs var:ticket.description ?></textarea>
+    </td>
+   </tr><tr>
+    <th><label for="reporter">Reporter:</label></th>
+    <td class="fullrow" colspan="3"><input type="text" value="<?cs 
+      var:ticket.reporter ?>" id="reporter" name="reporter" size="70" /></td>
+   </tr><?cs
+   /if ?>
+  <tr><?cs set:num_fields = 0 ?><?cs
+  each:field = ticket.fields ?><?cs
+   if:!field.skip ?><?cs
+    set:num_fields = num_fields + 1 ?><?cs
+   /if ?><?cs
+  /each ?><?cs set:idx = 0 ?><?cs
+   each:field = ticket.fields ?><?cs
+    if:!field.skip ?><?cs set:fullrow = field.type == 'textarea' ?><?cs
+     if:fullrow && idx % 2 ?><?cs set:idx = idx + 1 ?><th class="col2"></th><td></td></tr><tr><?cs /if ?>
+     <th class="col<?cs var:idx % 2 + 1 ?>"><?cs
+       if:field.type != 'radio' ?><label for="<?cs var:name(field) ?>"><?cs
+       /if ?><?cs alt:field.label ?><?cs var:field.name ?><?cs /alt ?>:<?cs
+       if:field.type != 'radio' ?></label><?cs /if ?></th>
+     <td<?cs if:fullrow ?> colspan="3"<?cs /if ?>><?cs
+      if:field.type == 'text' ?><input type="text" id="<?cs
+        var:name(field) ?>" name="<?cs
+        var:name(field) ?>" value="<?cs var:ticket[name(field)] ?>" /><?cs
+      elif:field.type == 'select' ?><select id="<?cs
+        var:name(field) ?>" name="<?cs
+        var:name(field) ?>"><?cs
+        if:field.optional ?><option></option><?cs /if ?><?cs
+        each:option = field.options ?><option<?cs
+         if:option == ticket[name(field)] ?> selected="selected"<?cs /if ?>><?cs
+         var:option ?></option><?cs
+        /each ?></select><?cs
+      elif:field.type == 'checkbox' ?><input type="hidden" name="checkbox_<?cs
+        var:name(field) ?>" /><input type="checkbox" id="<?cs
+        var:name(field) ?>" name="<?cs
+        var:name(field) ?>" value="1"<?cs
+        if:ticket[name(field)] ?> checked="checked"<?cs /if ?> /><?cs
+      elif:field.type == 'textarea' ?><textarea id="<?cs
+        var:name(field) ?>" name="<?cs
+        var:name(field) ?>"<?cs
+        if:field.height ?> rows="<?cs var:field.height ?>"<?cs /if ?><?cs
+        if:field.width ?> cols="<?cs var:field.width ?>"<?cs /if ?>>
+<?cs var:ticket[name(field)] ?></textarea><?cs
+      elif:field.type == 'radio' ?><?cs set:optidx = 0 ?><?cs
+       each:option = field.options ?><label><input type="radio" id="<?cs
+         var:name(field) ?>" name="<?cs
+         var:name(field) ?>" value="<?cs var:option ?>"<?cs
+         if:ticket[name(field)] == option ?> checked="checked"<?cs /if ?> /> <?cs
+         var:option ?></label> <?cs set:optidx = optidx + 1 ?><?cs
+        /each ?><?cs
+      /if ?></td><?cs
+     if:idx % 2 || fullrow ?><?cs
+      if:idx < num_fields - 1 ?></tr><tr><?cs
+      /if ?><?cs 
+     elif:idx == num_fields - 1 ?><th class="col2"></th><td></td><?cs
+     /if ?><?cs set:idx = idx + #fullrow + 1 ?><?cs
+    /if ?><?cs
+   /each ?></tr>
+  </table>
+ </fieldset><?cs /if ?>
+
+ <?cs if:ticket.actions.accept || ticket.actions.reopen ||
+         ticket.actions.resolve || ticket.actions.reassign ?>
+ <fieldset id="action">
+  <legend>Action</legend><?cs
+  if:!ticket.action ?><?cs set:ticket.action = 'leave' ?><?cs
+  /if ?><?cs
+  def:action_radio(id) ?>
+   <input type="radio" id="<?cs var:id ?>" name="action" value="<?cs
+     var:id ?>"<?cs if:ticket.action == id ?> checked="checked"<?cs
+     /if ?> /><?cs
+  /def ?>
+  <?cs call:action_radio('leave') ?>
+   <label for="leave">leave as <?cs var:ticket.status ?></label><br /><?cs
+  if:ticket.actions.accept ?><?cs
+   call:action_radio('accept') ?>
+   <label for="accept">accept ticket</label><br /><?cs
+  /if ?><?cs
+  if:ticket.actions.reopen ?><?cs
+   call:action_radio('reopen') ?>
+   <label for="reopen">reopen ticket</label><br /><?cs
+  /if ?><?cs
+  if:ticket.actions.resolve ?><?cs
+   call:action_radio('resolve') ?>
+   <label for="resolve">resolve</label><?cs
+   if:len(ticket.fields.resolution.options) ?>
+    <label for="resolve_resolution">as:</label>
+    <?cs call:hdf_select(ticket.fields.resolution.options, "resolve_resolution",
+                         ticket.resolve_resolution, 0) ?><br /><?cs
+   /if ?><?cs
+  /if ?><?cs
+  if:ticket.actions.reassign ?><?cs
+   call:action_radio('reassign') ?>
+   <label for="reassign">reassign</label>
+   <label>to:<?cs
+   if:len(ticket.fields.owner.options) ?><?cs
+    call:hdf_select(ticket.fields.owner.options, "reassign_owner",
+                    ticket.reassign_owner, 1) ?><?cs
+   else ?>
+    <input type="text" id="reassign_owner" name="reassign_owner" size="40" value="<?cs
+      var:ticket.reassign_owner ?>" /><?cs
+   /if ?></label><?cs
+  /if ?><?cs
+  if ticket.actions.resolve || ticket.actions.reassign ?>
+   <script type="text/javascript"><?cs
+    each:action = ticket.actions ?>
+     var <?cs var:name(action) ?> = document.getElementById("<?cs var:name(action) ?>");<?cs
+    /each ?>
+     var updateActionFields = function() {
+       <?cs if:ticket.actions.resolve ?> enableControl('resolve_resolution', resolve.checked);<?cs /if ?>
+       <?cs if:ticket.actions.reassign ?> enableControl('reassign_owner', reassign.checked);<?cs /if ?>
+     };
+     addEvent(window, 'load', updateActionFields);<?cs
+     each:action = ticket.actions ?>
+      addEvent(<?cs var:name(action) ?>, 'click', updateActionFields);<?cs
+     /each ?>
+   </script><?cs
+  /if ?>
+ </fieldset><?cs
+ else ?>
+  <input type="hidden" name="action" value="leave" /><?cs
+ /if ?>
+
+ <script type="text/javascript" src="<?cs
+   var:htdocs_location ?>js/wikitoolbar.js"></script>
+
+ <div class="buttons">
+  <input type="hidden" name="ts" value="<?cs var:ticket.ts ?>" />
+  <input type="hidden" name="replyto" value="<?cs var:ticket.replyto ?>" />
+  <input type="hidden" name="cnum" value="<?cs var:ticket.cnum ?>" />
+  <input type="submit" name="preview" value="Preview" accesskey="r" />&nbsp;
+  <input type="submit" value="Submit changes" />
+ </div>
+</form>
+<?cs /if ?>
+
+ </div>
+</div>
+<?cs include "footer.cs"?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/ticket_notify_email.cs
@@ -0,0 +1,23 @@
+<?cs var:email.ticket_body_hdr ?>
+<?cs var:email.ticket_props ?><?cs 
+if:ticket.new ?>
+<?cs var:ticket.description ?>
+<?cs else ?><?cs 
+ if:email.changes_body ?>
+Changes (by <?cs var:ticket.change.author ?>):
+
+<?cs var:email.changes_body ?><?cs
+ /if ?><?cs 
+var:email.changes_descr 
+?><?cs if:ticket.change.comment ?>
+Comment<?cs 
+ if:!email.changes_body ?> (by <?cs 
+   var:ticket.change.author ?>)<?cs /if ?>:
+
+<?cs var:ticket.change.comment ?>
+<?cs /if ?><?cs 
+/if ?>
+-- 
+Ticket URL: <<?cs var:ticket.link ?>>
+<?cs var:project.name ?> <<?cs var:project.url ?>>
+<?cs var:project.descr ?>
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/ticket_rss.cs
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<!-- RSS generated by Trac v<?cs var:trac.version ?> on <?cs var:trac.time ?> -->
+<rss version="2.0">
+ <channel><?cs 
+  if:project.name_encoded ?>
+   <title><?cs var:project.name_encoded ?>: Ticket <?cs var:title ?></title><?cs 
+  else ?>
+   <title>Ticket <?cs var:title ?></title><?cs 
+  /if ?>
+  <link><?cs var:base_host ?><?cs var:ticket.href ?></link>
+  <description><?cs var:ticket.description.formatted ?></description>
+  <language>en-us</language>
+  <generator>Trac v<?cs var:trac.version ?></generator><?cs 
+  each:change = ticket.changes ?>
+   <item><?cs
+    if:change.author ?><author><?cs var:change.author ?></author><?cs
+    /if ?>
+    <pubDate><?cs var:change.http_date ?></pubDate>
+    <title><?cs var:change.title ?></title>
+    <link><?cs var:base_host ?><?cs var:ticket.href ?><?cs 
+     if:change.cnum ?>#comment:<?cs var:change.cnum ?><?cs
+     /if ?></link>
+    <description>
+    <?cs if:len(change.fields) ?>
+    &lt;ul&gt;<?cs
+    each:field = change.fields ?>
+    &lt;li&gt;&lt;strong&gt;<?cs name:field ?>&lt;/strong&gt; <?cs
+     if:!field.old ?>set to &lt;em&gt;<?cs
+      var:field.new ?>&lt;/em&gt;<?cs
+     elif:field.new ?>changed from &lt;em&gt;<?cs var:field.old
+      ?>&lt;/em&gt; to &lt;em&gt;<?cs
+      var:field.new ?>&lt;/em&gt;.<?cs
+     else ?>deleted<?cs
+     /if ?>&lt;/li&gt;<?cs
+    /each ?>
+    &lt;/ul&gt;
+    <?cs /if ?>
+    <?cs var:change.comment ?>
+    </description>
+    <category>Ticket</category>
+   </item><?cs 
+  /each ?>
+ </channel>
+</rss>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/timeline.cs
@@ -0,0 +1,51 @@
+<?cs include "header.cs"?>
+
+<div id="ctxtnav" class="nav"></div>
+
+<div id="content" class="timeline">
+<h1>Timeline</h1>
+
+<form id="prefs" method="get" action="<?cs var:trac.href.timeline ?>">
+ <div>
+  <label>View changes from <input type="text" size="10" name="from" value="<?cs
+    var:timeline.from ?>" /></label> and
+  <label><input type="text" size="3" name="daysback" value="<?cs
+    var:timeline.daysback ?>" /> days back</label>.
+ </div>
+ <fieldset><?cs
+  each:filter = timeline.filters ?>
+   <label><input type="checkbox" name="<?cs var:filter.name ?>"<?cs
+     if:filter.enabled ?> checked="checked"<?cs /if ?> /> <?cs
+     var:filter.label ?></label><?cs
+  /each ?>
+ </fieldset>
+ <div class="buttons">
+  <input type="submit" name="update" value="Update" />
+ </div>
+</form><?cs
+
+def:day_separator(date) ?><?cs
+ if:date != current_date ?><?cs
+  if:current_date ?></dl><?cs /if ?><?cs
+  set:current_date = date ?>
+  <h2><?cs var:date ?>:</h2><dl><?cs
+ /if ?><?cs
+/def ?><?cs
+each:event = timeline.events ?><?cs
+ call:day_separator(event.date) ?><dt class="<?cs
+ var:event.kind ?>"><a href="<?cs var:event.href ?>"><span class="time"><?cs
+ var:event.time ?></span> <?cs var:event.title ?></a></dt><?cs
+  if:event.message ?><dd class="<?cs var:event.kind ?>"><?cs
+   var:event.message ?></dd><?cs
+  /if ?><?cs
+/each ?><?cs
+if:len(timeline.events) ?></dl><?cs /if ?>
+
+<div id="help">
+ <hr />
+ <strong>Note:</strong> See <a href="<?cs var:trac.href.wiki ?>/TracTimeline">TracTimeline</a> 
+ for information about the timeline view.
+</div>
+
+</div>
+<?cs include "footer.cs"?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/timeline_rss.cs
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+<rss version="2.0">
+ <channel><?cs
+  if:project.name_encoded ?>
+   <title><?cs var:project.name_encoded ?>: <?cs var:title ?></title><?cs
+  else ?>
+   <title><?cs var:title ?></title><?cs
+  /if ?>
+  <link><?cs var:base_host ?><?cs var:trac.href.timeline ?></link>
+  <description>Trac Timeline</description>
+  <language>en-us</language>
+  <generator>Trac v<?cs var:trac.version ?></generator><?cs
+  if:chrome.logo.src ?>
+   <image>
+    <title><?cs var:project.name_encoded ?></title>
+    <url><?cs if:!chrome.logo.src_abs ?><?cs var:base_host ?><?cs /if ?><?cs
+     var:chrome.logo.src ?></url>
+    <link><?cs var:base_host ?><?cs var:trac.href.timeline ?></link>
+   </image><?cs
+  /if ?><?cs
+  each:event = timeline.events ?>
+   <item>
+    <title><?cs var:event.title ?></title><?cs
+    if:event.author.email ?>
+     <author><?cs var:event.author.email ?></author><?cs
+    /if ?>
+    <pubDate><?cs var:event.date ?></pubDate>
+    <link><?cs var:event.href ?></link>
+    <description><?cs var:event.message ?></description>
+   </item><?cs
+  /each ?>
+ </channel>
+</rss>
new file mode 100644
--- /dev/null
+++ b/examples/trac/templates/wiki.cs
@@ -0,0 +1,390 @@
+<?cs include "header.cs" ?>
+<?cs include "macros.cs" ?>
+
+<div id="ctxtnav" class="nav">
+ <h2>Wiki Navigation</h2>
+ <ul><?cs
+  if:wiki.action == "diff" ?>
+   <li class="first"><?cs
+     if:len(chrome.links.prev) ?> &larr; 
+      <a class="prev" href="<?cs var:chrome.links.prev.0.href ?>" title="<?cs
+       var:chrome.links.prev.0.title ?>">Previous Change</a><?cs
+     else ?>
+      <span class="missing">&larr; Previous Change</span><?cs
+     /if ?>
+   </li>
+   <li><a href="<?cs var:wiki.history_href ?>">Page History</a></li>
+   <li class="last"><?cs
+     if:len(chrome.links.next) ?>
+      <a class="next" href="<?cs var:chrome.links.next.0.href ?>" title="<?cs
+       var:chrome.links.next.0.title ?>">Next Change</a> &rarr; <?cs
+     else ?>
+      <span class="missing">Next Change &rarr;</span><?cs
+     /if ?>
+   </li><?cs
+  elif:wiki.action == "history" ?>
+   <li><a href="<?cs var:wiki.current_href ?>">View Latest Version</a></li><?cs
+  else ?>
+   <li><a href="<?cs var:trac.href.wiki ?>">Start Page</a></li>
+   <li><a href="<?cs var:trac.href.wiki ?>/TitleIndex">Index by Title</a></li>
+   <li><a href="<?cs var:trac.href.wiki ?>/RecentChanges">Index by Date</a></li>
+   <li class="last"><a href="<?cs var:wiki.last_change_href ?>">Last Change</a></li><?cs 
+  /if ?>
+ </ul>
+ <hr />
+</div>
+
+<div id="content" class="wiki">
+
+ <?cs if wiki.action == "delete" ?><?cs 
+  if:wiki.version - wiki.old_version > 1 ?><?cs
+   set:first_version = wiki.old_version + 1 ?><?cs
+   set:version_range = "versions "+first_version+" to "+wiki.version+" of " ?><?cs
+   set:delete_what = "those versions" ?><?cs
+  elif:wiki.version ?><?cs
+   set:version_range = "version "+wiki.version+" of " ?><?cs
+   set:delete_what = "this version" ?><?cs
+  else ?><?cs
+   set:version_range = "" ?><?cs
+   set:delete_what = "page" ?><?cs
+  /if ?>
+  <h1>Delete <?cs var:version_range ?><a href="<?cs
+    var:wiki.current_href ?>"><?cs var:wiki.page_name ?></a></h1>
+  <form action="<?cs var:wiki.current_href ?>" method="post">
+   <input type="hidden" name="action" value="delete" />
+   <p><strong>Are you sure you want to <?cs
+    if:!?wiki.version ?>completely <?cs 
+    /if ?>delete <?cs var:version_range ?>this page?</strong><br /><?cs
+   if:wiki.only_version ?>
+    This is the only version the page, so the page will be removed
+    completely!<?cs
+   /if ?><?cs
+   if:?wiki.version ?>
+    <input type="hidden" name="version" value="<?cs var:wiki.version ?>" /><?cs
+   /if ?><?cs
+   if:wiki.old_version ?>
+    <input type="hidden" name="old_version" value="<?cs var:wiki.old_version ?>" /><?cs
+   /if ?>
+   This is an irreversible operation.</p>
+   <div class="buttons">
+    <input type="submit" name="cancel" value="Cancel" />
+    <input type="submit" value="Delete <?cs var:delete_what ?>" />
+   </div>
+  </form>
+ 
+ <?cs elif:wiki.action == "diff" ?>
+  <h1>Changes <?cs
+    if:wiki.old_version ?>between 
+     <a href="<?cs var:wiki.current_href ?>?version=<?cs var:wiki.old_version?>">Version <?cs var:wiki.old_version?></a> and <?cs
+    else ?>from <?cs
+    /if ?>
+    <a href="<?cs var:wiki.current_href ?>?version=<?cs var:wiki.version?>">Version <?cs var:wiki.version?></a> of 
+    <a href="<?cs var:wiki.current_href ?>"><?cs var:wiki.page_name ?></a></h1>
+  <form method="post" id="prefs" action="<?cs var:wiki.current_href ?>">
+   <div>
+    <input type="hidden" name="action" value="diff" />
+    <input type="hidden" name="version" value="<?cs var:wiki.version ?>" />
+    <label>View differences <select name="style">
+     <option value="inline"<?cs
+       if:diff.style == 'inline' ?> selected="selected"<?cs
+       /if ?>>inline</option>
+     <option value="sidebyside"<?cs
+       if:diff.style == 'sidebyside' ?> selected="selected"<?cs
+       /if ?>>side by side</option>
+    </select></label>
+    <div class="field">
+     Show <input type="text" name="contextlines" id="contextlines" size="2"
+       maxlength="3" value="<?cs var:diff.options.contextlines ?>" />
+     <label for="contextlines">lines around each change</label>
+    </div>
+    <fieldset id="ignore">
+     <legend>Ignore:</legend>
+     <div class="field">
+      <input type="checkbox" id="blanklines" name="ignoreblanklines"<?cs
+        if:diff.options.ignoreblanklines ?> checked="checked"<?cs /if ?> />
+      <label for="blanklines">Blank lines</label>
+     </div>
+     <div class="field">
+      <input type="checkbox" id="case" name="ignorecase"<?cs
+        if:diff.options.ignorecase ?> checked="checked"<?cs /if ?> />
+      <label for="case">Case changes</label>
+     </div>
+     <div class="field">
+      <input type="checkbox" id="whitespace" name="ignorewhitespace"<?cs
+        if:diff.options.ignorewhitespace ?> checked="checked"<?cs /if ?> />
+      <label for="whitespace">White space changes</label>
+     </div>
+    </fieldset>
+    <div class="buttons">
+     <input type="submit" name="update" value="Update" />
+    </div>
+   </div>
+  </form>
+  <dl id="overview">
+   <dt class="property author">Author:</dt>
+   <dd class="author"><?cs
+    if:wiki.num_changes > 1 ?><em class="multi">(multiple changes)</em><?cs
+    else ?><?cs var:wiki.author ?> <span class="ipnr">(IP: <?cs
+     var:wiki.ipnr ?>)</span><?cs
+    /if ?></dd>
+   <dt class="property time">Timestamp:</dt>
+   <dd class="time"><?cs
+    if:wiki.num_changes > 1 ?><em class="multi">(multiple changes)</em><?cs
+    elif:wiki.time ?><?cs var:wiki.time ?> (<?cs var:wiki.time_delta ?> ago)<?cs
+    else ?>--<?cs
+    /if ?></dd>
+   <dt class="property message">Comment:</dt>
+   <dd class="message"><?cs
+    if:wiki.num_changes > 1 ?><em class="multi">(multiple changes)</em><?cs
+    else ?><?cs var:wiki.comment ?><?cs /if ?></dd>
+  </dl>
+  <div class="diff">
+   <div id="legend">
+    <h3>Legend:</h3>
+    <dl>
+     <dt class="unmod"></dt><dd>Unmodified</dd>
+     <dt class="add"></dt><dd>Added</dd>
+     <dt class="rem"></dt><dd>Removed</dd>
+     <dt class="mod"></dt><dd>Modified</dd>
+    </dl>
+   </div>
+   <ul class="entries">
+    <li class="entry">
+     <h2><?cs var:wiki.page_name ?></h2><?cs
+      if:diff.style == 'sidebyside' ?>
+      <table class="sidebyside" summary="Differences">
+       <colgroup class="l"><col class="lineno" /><col class="content" /></colgroup>
+       <colgroup class="r"><col class="lineno" /><col class="content" /></colgroup>
+       <thead><tr>
+        <th colspan="2">Version <?cs var:wiki.old_version ?></th>
+        <th colspan="2">Version <?cs var:wiki.version ?></th>
+       </tr></thead><?cs
+       each:change = wiki.diff ?><?cs
+        call:diff_display(change, diff.style) ?><?cs
+       /each ?>
+      </table><?cs
+     else ?>
+      <table class="inline" summary="Differences">
+       <colgroup><col class="lineno" /><col class="lineno" /><col class="content" /></colgroup>
+       <thead><tr>
+        <th title="Version <?cs var:wiki.old_version ?>">v<?cs
+          var:wiki.old_version ?></th>
+        <th title="Version <?cs var:wiki.version ?>">v<?cs
+          var:wiki.version ?></th>
+        <th>&nbsp;</th>
+       </tr></thead><?cs
+       each:change = wiki.diff ?><?cs
+        call:diff_display(change, diff.style) ?><?cs
+       /each ?>
+      </table><?cs
+     /if ?>
+    </li>
+   </ul><?cs
+   if:trac.acl.WIKI_DELETE && 
+    (len(wiki.diff) == 0 || wiki.version == wiki.latest_version) ?>
+    <form method="get" action="<?cs var:wiki.current_href ?>">
+     <input type="hidden" name="action" value="delete" />
+     <input type="hidden" name="version" value="<?cs var:wiki.version ?>" />
+     <input type="hidden" name="old_version" value="<?cs var:wiki.old_version ?>" />
+     <input type="submit" name="delete_version" value="Delete <?cs
+     if:wiki.version - wiki.old_version > 1 ?> version <?cs 
+      var:wiki.old_version+1 ?> to <?cs 
+     /if ?>version <?cs var:wiki.version ?>" />
+    </form><?cs
+   /if ?>
+  </div>
+
+ <?cs elif wiki.action == "history" ?>
+  <h1>Change History of <a href="<?cs var:wiki.current_href ?>"><?cs
+    var:wiki.page_name ?></a></h1>
+  <?cs if:len(wiki.history) ?><form class="printableform" method="get" action="">
+   <input type="hidden" name="action" value="diff" />
+   <div class="buttons">
+    <input type="submit" value="View changes" />
+   </div>
+   <table id="wikihist" class="listing" summary="Change history">
+    <thead><tr>
+     <th class="diff"></th>
+     <th class="version">Version</th>
+     <th class="date">Date</th>
+     <th class="author">Author</th>
+     <th class="comment">Comment</th>
+    </tr></thead>
+    <tbody><?cs each:item = wiki.history ?>
+     <tr class="<?cs if:name(item) % #2 ?>even<?cs else ?>odd<?cs /if ?>">
+      <td class="diff"><input type="radio" name="old_version" value="<?cs
+        var:item.version ?>"<?cs
+        if:name(item) == 1 ?> checked="checked"<?cs
+        /if ?> /> <input type="radio" name="version" value="<?cs
+        var:item.version ?>"<?cs
+        if:name(item) == 0 ?> checked="checked"<?cs
+        /if ?> /></td>
+      <td class="version"><a href="<?cs
+        var:item.url ?>" title="View this version"><?cs
+        var:item.version ?></a></td>
+      <td class="date"><?cs var:item.time ?></td>
+      <td class="author" title="IP-Address: <?cs var:item.ipaddr ?>"><?cs 
+        var:item.author ?></td>
+      <td class="comment"><?cs var:item.comment ?></td>
+     </tr>
+    <?cs /each ?></tbody>
+   </table><?cs
+   if:len(wiki.history) > #10 ?>
+    <div class="buttons">
+     <input type="submit" value="View changes" />
+    </div><?cs
+   /if ?>
+  </form><?cs /if ?>
+ 
+ <?cs else ?>
+  <?cs if wiki.action == "edit" || wiki.action == "preview" || wiki.action == "collision" ?>
+   <h1>Editing "<?cs var:wiki.page_name ?>"</h1><?cs
+    if wiki.action == "preview" ?>
+     <table id="info" summary="Revision info"><tbody><tr>
+       <th scope="col">
+        Preview of future version <?cs var:$wiki.version+1 ?> (modified by <?cs var:wiki.author ?>)
+       </th></tr><tr>
+       <td class="message"><?cs var:wiki.comment_html ?></td>
+      </tr>
+     </tbody></table>
+     <fieldset id="preview">
+      <legend>Preview (<a href="#edit">skip</a>)</legend>
+        <div class="wikipage"><?cs var:wiki.page_html ?></div>
+     </fieldset><?cs
+     elif wiki.action =="collision"?>
+     <div class="system-message">
+       Sorry, this page has been modified by somebody else since you started 
+       editing. Your changes cannot be saved.
+     </div><?cs
+    /if ?>
+   <form id="edit" action="<?cs var:wiki.current_href ?>" method="post">
+    <fieldset class="iefix">
+     <input type="hidden" name="action" value="edit" />
+     <input type="hidden" name="version" value="<?cs var:wiki.version ?>" />
+     <input type="hidden" id="scroll_bar_pos" name="scroll_bar_pos" value="<?cs
+       var:wiki.scroll_bar_pos ?>" />
+     <div id="rows">
+      <label for="editrows">Adjust edit area height:</label>
+      <select size="1" name="editrows" id="editrows" tabindex="43"
+        onchange="resizeTextArea('text', this.options[selectedIndex].value)"><?cs
+       loop:rows = 8, 42, 4 ?>
+        <option value="<?cs var:rows ?>"<?cs
+          if:rows == wiki.edit_rows ?> selected="selected"<?cs /if ?>><?cs
+          var:rows ?></option><?cs
+       /loop ?>
+      </select>
+     </div>
+     <p><textarea id="text" class="wikitext" name="text" cols="80" rows="<?cs
+       var:wiki.edit_rows ?>">
+<?cs var:wiki.page_source ?></textarea></p>
+     <script type="text/javascript">
+       var scrollBarPos = document.getElementById("scroll_bar_pos");
+       var text = document.getElementById("text");
+       addEvent(window, "load", function() {
+         if (scrollBarPos.value) text.scrollTop = scrollBarPos.value;
+       });
+       addEvent(text, "blur", function() { scrollBarPos.value = text.scrollTop });
+     </script>
+    </fieldset>
+    <div id="help">
+     <b>Note:</b> See <a href="<?cs var:$trac.href.wiki
+?>/WikiFormatting">WikiFormatting</a> and <a href="<?cs var:$trac.href.wiki
+?>/TracWiki">TracWiki</a> for help on editing wiki content.
+    </div>
+    <fieldset id="changeinfo">
+     <legend>Change information</legend>
+     <?cs if:trac.authname == "anonymous" ?>
+      <div class="field">
+       <label>Your email or username:<br />
+       <input id="author" type="text" name="author" size="30" value="<?cs
+         var:wiki.author ?>" /></label>
+      </div>
+     <?cs /if ?>
+     <div class="field">
+      <label>Comment about this change (optional):<br />
+      <input id="comment" type="text" name="comment" size="60" value="<?cs
+        var:wiki.comment?>" /></label>
+     </div><br />
+     <?cs if trac.acl.WIKI_ADMIN ?>
+      <div class="options">
+       <label><input type="checkbox" name="readonly" id="readonly"<?cs
+         if wiki.readonly == "1"?>checked="checked"<?cs /if ?> />
+       Page is read-only</label>
+      </div>
+     <?cs /if ?>
+    </fieldset>
+    <div class="buttons"><?cs
+     if wiki.action == "collision" ?>
+      <input type="submit" name="preview" value="Preview" disabled="disabled" />&nbsp;
+      <input type="submit" name="save" value="Submit changes" disabled="disabled" />&nbsp;
+     <?cs else ?>
+      <input type="submit" name="preview" value="Preview" accesskey="r" />&nbsp;
+      <input type="submit" name="save" value="Submit changes" />&nbsp;
+     <?cs /if ?>
+     <input type="submit" name="cancel" value="Cancel" />
+    </div>
+    <script type="text/javascript" src="<?cs
+      var:htdocs_location ?>js/wikitoolbar.js"></script>
+   </form>
+  <?cs /if ?>
+  <?cs if wiki.action == "view" ?>
+   <?cs if:wiki.comment_html ?>
+    <table id="info" summary="Revision info"><tbody><tr>
+      <th scope="col">
+       Version <?cs var:wiki.version ?> (modified by <?cs var:wiki.author ?>, <?cs var:wiki.age ?> ago)
+      </th></tr><tr>
+      <td class="message"><?cs var:wiki.comment_html ?></td>
+     </tr>
+    </tbody></table>
+   <?cs /if ?>
+   <div class="wikipage">
+    <div id="searchable"><?cs var:wiki.page_html ?></div>
+   </div>
+   <?cs if:len(wiki.attachments) ?>
+    <h3 id="tkt-changes-hdr">Attachments</h3>
+    <ul class="tkt-chg-list"><?cs
+     each:attachment = wiki.attachments ?><li class="tkt-chg-change"><a href="<?cs
+      var:attachment.href ?>"><?cs
+      var:attachment.filename ?></a> (<?cs var:attachment.size ?>) -<?cs
+      if:attachment.description ?><q><?cs var:attachment.description ?></q>,<?cs
+      /if ?> added by <?cs var:attachment.author ?> on <?cs
+      var:attachment.time ?>.</li><?cs
+     /each ?>
+    </ul>
+  <?cs /if ?>
+  <?cs if wiki.action == "view" && (trac.acl.WIKI_MODIFY || trac.acl.WIKI_DELETE)
+      && (wiki.readonly == "0" || trac.acl.WIKI_ADMIN) ?>
+   <div class="buttons"><?cs
+    if:trac.acl.WIKI_MODIFY ?>
+     <form method="get" action="<?cs var:wiki.current_href ?>"><div>
+      <input type="hidden" name="action" value="edit" />
+      <input type="submit" value="<?cs if:wiki.exists ?>Edit<?cs
+        else ?>Create<?cs /if ?> this page" accesskey="e" />
+     </div></form><?cs
+     if:wiki.exists ?>
+      <form method="get" action="<?cs var:wiki.attach_href ?>"><div>
+       <input type="hidden" name="action" value="new" />
+       <input type="submit" value="Attach file" />
+      </div></form><?cs
+     /if ?><?cs
+    /if ?><?cs
+    if:wiki.exists && trac.acl.WIKI_DELETE ?>
+     <form method="get" action="<?cs var:wiki.current_href ?>"><div id="delete">
+      <input type="hidden" name="action" value="delete" />
+      <input type="hidden" name="version" value="<?cs var:wiki.version ?>" /><?cs
+      if:wiki.version == wiki.latest_version ?>
+       <input type="submit" name="delete_version" value="Delete this version" /><?cs
+      /if ?>
+      <input type="submit" value="Delete page" />
+     </div></form>
+    <?cs /if ?>
+   </div>
+  <?cs /if ?>
+  <script type="text/javascript">
+   addHeadingLinks(document.getElementById("searchable"));
+  </script>
+ <?cs /if ?>
+ <?cs /if ?>
+</div>
+
+<?cs include "footer.cs" ?>
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/About.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2006 Edgewall Software
+# Copyright (C) 2004-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2004-2005 Daniel Lundin <daniel@edgewall.com>
+# Copyright (C) 2005-2006 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+import re
+
+from trac.core import *
+from trac.perm import IPermissionRequestor
+from trac.web import IRequestHandler
+from trac.util.markup import html
+from trac.web.chrome import add_stylesheet, INavigationContributor
+
+
+class AboutModule(Component):
+    """Provides various about pages."""
+
+    implements(INavigationContributor, IPermissionRequestor, IRequestHandler)
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'about'
+
+    def get_navigation_items(self, req):
+        yield ('metanav', 'about',
+               html.a('About Trac', href=req.href.about()))
+
+    # IPermissionRequestor methods
+
+    def get_permission_actions(self):
+        return ['CONFIG_VIEW']
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        match = re.match(r'/about(?:_trac)?(?:/(.*))?$', req.path_info)
+        if match:
+            if match.group(1):
+                req.args['page'] = match.group(1)
+            return True
+
+    def process_request(self, req):
+        page = req.args.get('page', 'default')
+        req.hdf['title'] = 'About Trac'
+        if req.perm.has_permission('CONFIG_VIEW'):
+            req.hdf['about.config_href'] = req.href.about('config')
+            req.hdf['about.plugins_href'] = req.href.about('plugins')
+        if page == 'config':
+            self._render_config(req)
+        elif page == 'plugins':
+            self._render_plugins(req)
+
+        add_stylesheet(req, 'common/css/about.css')
+        return 'about.cs', None
+
+    # Internal methods
+
+    def _render_config(self, req):
+        req.perm.assert_permission('CONFIG_VIEW')
+        req.hdf['about.page'] = 'config'
+        
+        # Export the config table to hdf
+        sections = []
+        for section in self.config.sections():
+            options = []
+            default_options = self.config.defaults().get(section)
+            for name,value in self.config.options(section):
+                default = default_options and default_options.get(name) or ''
+                options.append({
+                    'name': name, 'value': value,
+                    'valueclass': (unicode(value) == unicode(default) 
+                                   and 'defaultvalue' or 'value')})
+            options.sort(lambda x,y: cmp(x['name'], y['name']))
+            sections.append({'name': section, 'options': options})
+        sections.sort(lambda x,y: cmp(x['name'], y['name']))
+        req.hdf['about.config'] = sections
+        # TODO:
+        # We should probably export more info here like:
+        # permissions, components...
+
+    def _render_plugins(self, req):
+        try:
+            from trac.wiki.formatter import wiki_to_html
+            import inspect
+            def getdoc(obj):
+                return wiki_to_html(inspect.getdoc(obj), self.env, req)
+        except:
+            def getdoc(obj):
+                return obj.__doc__
+        req.perm.assert_permission('CONFIG_VIEW')
+        import sys
+        req.hdf['about.page'] = 'plugins'
+        from trac.core import ComponentMeta
+        plugins = []
+        for component in ComponentMeta._components:
+            if not self.env.is_component_enabled(component):
+                continue
+            plugin = {'name': component.__name__}
+            if component.__doc__:
+                plugin['description'] = getdoc(component)
+
+            module = sys.modules[component.__module__]
+            plugin['module'] = module.__name__
+            if hasattr(module, '__file__'):
+                plugin['path'] = module.__file__
+
+            xtnpts = []
+            for name, xtnpt in [(attr, getattr(component, attr)) for attr
+                                in dir(component)]:
+                if not isinstance(xtnpt, ExtensionPoint):
+                    continue
+                xtnpts.append({'name': name,
+                               'interface': xtnpt.interface.__name__,
+                               'module': xtnpt.interface.__module__})
+                if xtnpt.interface.__doc__:
+                    xtnpts[-1]['description'] = getdoc(xtnpt.interface)
+                extensions = []
+                for extension in ComponentMeta._registry.get(xtnpt.interface, []):
+                    if self.env.is_component_enabled(extension):
+                        extensions.append({'name': extension.__name__,
+                                           'module': extension.__module__})
+                xtnpts[-1]['extensions'] = extensions
+            xtnpts.sort(lambda x,y: cmp(x['name'], y['name']))
+            plugin['extension_points'] = xtnpts
+
+            plugins.append(plugin)
+
+        def plugincmp(x, y):
+            c = cmp(len(x['module'].split('.')), len(y['module'].split('.')))
+            if c == 0:
+                c = cmp(x['module'].lower(), y['module'].lower())
+                if c == 0:
+                    c = cmp(x['name'].lower(), y['name'].lower())
+            return c
+        plugins.sort(plugincmp)
+
+        req.hdf['about.plugins'] = plugins
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/Search.py
@@ -0,0 +1,248 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2004 Edgewall Software
+# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+
+import re
+import time
+
+from trac.config import IntOption
+from trac.core import *
+from trac.perm import IPermissionRequestor
+from trac.util.datefmt import format_datetime
+from trac.util.markup import escape, html, Element
+from trac.web import IRequestHandler
+from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
+from trac.wiki import IWikiSyntaxProvider, wiki_to_link
+
+
+class ISearchSource(Interface):
+    """
+    Extension point interface for adding search sources to the Trac
+    Search system.
+    """
+
+    def get_search_filters(self, req):
+        """
+        Return a list of filters that this search source supports. Each
+        filter must be a (name, label[, default]) tuple, where `name` is the
+        internal name, `label` is a human-readable name for display and
+        `default` is an optional boolean for determining whether this filter
+        is searchable by default.
+        """
+
+    def get_search_results(self, req, terms, filters):
+        """
+        Return a list of search results matching each search term in `terms`.
+        The `filters` parameters is a list of the enabled
+        filters, each item being the name of the tuples returned by
+        `get_search_events`.
+
+        The events returned by this function must be tuples of the form
+        (href, title, date, author, excerpt).
+        """
+
+
+def search_terms(q):
+    """
+    Break apart a search query into its various search terms.  Terms are
+    grouped implicitly by word boundary, or explicitly by (single or double)
+    quotes.
+    """
+    results = []
+    for term in re.split('(".*?")|(\'.*?\')|(\s+)', q):
+        if term != None and term.strip() != '':
+            if term[0] == term[-1] == "'" or term[0] == term[-1] == '"':
+                term = term[1:-1]
+            results.append(term)
+    return results
+
+def search_to_sql(db, columns, terms):
+    """
+    Convert a search query into a SQL condition string and corresponding
+    parameters. The result is returned as a (string, params) tuple.
+    """
+    if len(columns) < 1 or len(terms) < 1:
+        raise TracError('Empty search attempt, this should really not happen.')
+
+    likes = ['%s %s' % (i, db.like()) for i in columns]
+    c = ' OR '.join(likes)
+    sql = '(' + ') AND ('.join([c] * len(terms)) + ')'
+    args = []
+    for t in terms:
+        args.extend(['%'+db.like_escape(t)+'%'] * len(columns))
+    return sql, tuple(args)
+
+def shorten_result(text='', keywords=[], maxlen=240, fuzz=60):
+    if not text: text = ''
+    text_low = text.lower()
+    beg = -1
+    for k in keywords:
+        i = text_low.find(k.lower())
+        if (i > -1 and i < beg) or beg == -1:
+            beg = i
+    excerpt_beg = 0
+    if beg > fuzz:
+        for sep in ('.', ':', ';', '='):
+            eb = text.find(sep, beg - fuzz, beg - 1)
+            if eb > -1:
+                eb += 1
+                break
+        else:
+            eb = beg - fuzz
+        excerpt_beg = eb
+    if excerpt_beg < 0: excerpt_beg = 0
+    msg = text[excerpt_beg:beg+maxlen]
+    if beg > fuzz:
+        msg = '... ' + msg
+    if beg < len(text)-maxlen:
+        msg = msg + ' ...'
+    return msg
+    
+
+class SearchModule(Component):
+
+    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
+               IWikiSyntaxProvider)
+
+    search_sources = ExtensionPoint(ISearchSource)
+    
+    RESULTS_PER_PAGE = 10
+
+    min_query_length = IntOption('search', 'min_query_length', 3,
+        """Minimum length of query string allowed when performing a search.""")
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'search'
+
+    def get_navigation_items(self, req):
+        if not req.perm.has_permission('SEARCH_VIEW'):
+            return
+        yield ('mainnav', 'search',
+               html.A('Search', href=req.href.search(), accesskey=4))
+
+    # IPermissionRequestor methods
+
+    def get_permission_actions(self):
+        return ['SEARCH_VIEW']
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        return re.match(r'/search/?', req.path_info) is not None
+
+    def process_request(self, req):
+        req.perm.assert_permission('SEARCH_VIEW')
+
+        available_filters = []
+        for source in self.search_sources:
+            available_filters += source.get_search_filters(req)
+            
+        filters = [f[0] for f in available_filters if req.args.has_key(f[0])]
+        if not filters:
+            filters = [f[0] for f in available_filters
+                       if len(f) < 3 or len(f) > 2 and f[2]]
+                
+        req.hdf['search.filters'] = [
+            { 'name': filter[0],
+              'label': filter[1],
+              'active': filter[0] in filters
+            } for filter in available_filters]
+                
+        req.hdf['title'] = 'Search'
+
+        query = req.args.get('q')
+        if query:
+            page = int(req.args.get('page', '1'))
+            noquickjump = int(req.args.get('noquickjump', '0'))
+            link_elt = self.quickjump(req, query)
+            if link_elt is not None:
+                quickjump_href = link_elt.attr['href']
+                if noquickjump:
+                    req.hdf['search.quickjump'] = {
+                        'href': quickjump_href,
+                        'name': html.EM(link_elt.children),
+                        'description': link_elt.attr.get('title', '')
+                        }
+                else:
+                    req.redirect(quickjump_href)
+            elif query.startswith('!'):
+                query = query[1:]
+            terms = search_terms(query)
+            # Refuse queries that obviously would result in a huge result set
+            if len(terms) == 1 and len(terms[0]) < self.min_query_length:
+                raise TracError('Search query too short. '
+                                'Query must be at least %d characters long.' % \
+                                self.min_query_length, 'Search Error')
+            results = []
+            for source in self.search_sources:
+                results += list(source.get_search_results(req, terms, filters))
+            results.sort(lambda x,y: cmp(y[2], x[2]))
+            page_size = self.RESULTS_PER_PAGE
+            n = len(results)
+            n_pages = (n-1) / page_size + 1
+            results = results[(page-1) * page_size: page * page_size]
+
+            req.hdf['title'] = 'Search Results'
+            req.hdf['search.q'] = req.args.get('q')
+            req.hdf['search.page'] = page
+            req.hdf['search.n_hits'] = n
+            req.hdf['search.n_pages'] = n_pages
+            req.hdf['search.page_size'] = page_size
+            if page < n_pages:
+                next_href = req.href.search(zip(filters, ['on'] * len(filters)),
+                                            q=req.args.get('q'), page=page + 1)
+                add_link(req, 'next', next_href, 'Next Page')
+            if page > 1:
+                prev_href = req.href.search(zip(filters, ['on'] * len(filters)),
+                                            q=req.args.get('q'), page=page - 1)
+                add_link(req, 'prev', prev_href, 'Previous Page')
+            req.hdf['search.page_href'] = req.href.search(zip(filters, ['on'] * len(filters)),
+                                                          q=req.args.get('q'))
+            req.hdf['search.result'] = [
+                { 'href': result[0],
+                  'title': result[1],
+                  'date': format_datetime(result[2]),
+                  'author': result[3],
+                  'excerpt': result[4]
+                } for result in results]
+
+        add_stylesheet(req, 'common/css/search.css')
+        return 'search.cs', None
+
+    def quickjump(self, req, kwd):
+        # Source quickjump
+        if kwd[0] == '/':
+            return req.href.browser(kwd)
+        link = wiki_to_link(kwd, self.env, req)
+        if isinstance(link, Element):
+            return link
+
+    # IWikiSyntaxProvider methods
+    
+    def get_wiki_syntax(self):
+        return []
+    
+    def get_link_resolvers(self):
+        yield ('search', self._format_link)
+
+    def _format_link(self, formatter, ns, target, label):
+        path, query, fragment = formatter.split_link(target)
+        if query:
+            href = formatter.href.search() + query.replace(' ', '+')
+        else:
+            href = formatter.href.search(q=path)
+        return html.A(label, class_='search', href=href)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/Settings.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2005 Edgewall Software
+# Copyright (C) 2004-2005 Daniel Lundin <daniel@edgewall.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Daniel Lundin <daniel@edgewall.com>
+
+from trac.core import *
+from trac.util.markup import html
+from trac.web import IRequestHandler
+from trac.web.chrome import INavigationContributor
+
+
+class SettingsModule(Component):
+
+    implements(INavigationContributor, IRequestHandler)
+
+    _form_fields = ['newsid','name', 'email']
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'settings'
+
+    def get_navigation_items(self, req):
+        yield ('metanav', 'settings',
+               html.A('Settings', href=req.href.settings()))
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        return req.path_info == '/settings'
+
+    def process_request(self, req):
+        action = req.args.get('action')
+
+        if req.method == 'POST':
+            if action == 'save':
+                self._do_save(req)
+            elif action == 'load':
+                self._do_load(req)
+
+        req.hdf['title'] = 'Settings'
+        req.hdf['settings'] = req.session
+        if req.authname == 'anonymous':
+            req.hdf['settings.session_id'] = req.session.sid
+
+        return 'settings.cs', None
+
+    # Internal methods
+
+    def _do_save(self, req):
+        for field in self._form_fields:
+            val = req.args.get(field)
+            if val:
+                if field == 'newsid' and val:
+                    req.session.change_sid(val)
+                else:
+                    req.session[field] = val
+        req.redirect(req.href.settings())
+
+    def _do_load(self, req):
+        if req.authname == 'anonymous':
+            oldsid = req.args.get('loadsid')
+            req.session.get_session(oldsid)
+        req.redirect(req.href.settings())
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/Timeline.py
@@ -0,0 +1,222 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+import re
+import time
+
+from trac.config import IntOption
+from trac.core import *
+from trac.perm import IPermissionRequestor
+from trac.util.datefmt import format_date, format_time, http_date
+from trac.util.text import to_unicode
+from trac.util.markup import html, Markup
+from trac.web import IRequestHandler
+from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
+
+
+class ITimelineEventProvider(Interface):
+    """Extension point interface for adding sources for timed events to the
+    timeline.
+    """
+
+    def get_timeline_filters(self, req):
+        """Return a list of filters that this event provider supports.
+        
+        Each filter must be a (name, label) tuple, where `name` is the internal
+        name, and `label` is a human-readable name for display.
+
+        Optionally, the tuple can contain a third element, `checked`.
+        If `checked` is omitted or True, the filter is active by default,
+        otherwise it will be inactive.
+        """
+
+    def get_timeline_events(self, req, start, stop, filters):
+        """Return a list of events in the time range given by the `start` and
+        `stop` parameters.
+        
+        The `filters` parameters is a list of the enabled filters, each item
+        being the name of the tuples returned by `get_timeline_filters`.
+
+        The events returned by this function must be tuples of the form
+        (kind, href, title, date, author, message).
+        """
+
+
+class TimelineModule(Component):
+
+    implements(INavigationContributor, IPermissionRequestor, IRequestHandler)
+
+    event_providers = ExtensionPoint(ITimelineEventProvider)
+
+    default_daysback = IntOption('timeline', 'default_daysback', 30,
+        """Default number of days displayed in the Timeline, in days.
+        (''since 0.9.'')""")
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'timeline'
+
+    def get_navigation_items(self, req):
+        if not req.perm.has_permission('TIMELINE_VIEW'):
+            return
+        yield ('mainnav', 'timeline',
+               html.A('Timeline', href=req.href.timeline(), accesskey=2))
+
+    # IPermissionRequestor methods
+
+    def get_permission_actions(self):
+        return ['TIMELINE_VIEW']
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        return re.match(r'/timeline/?', req.path_info) is not None
+
+    def process_request(self, req):
+        req.perm.assert_permission('TIMELINE_VIEW')
+
+        format = req.args.get('format')
+        maxrows = int(req.args.get('max', 0))
+
+        # Parse the from date and adjust the timestamp to the last second of
+        # the day
+        t = time.localtime()
+        if req.args.has_key('from'):
+            try:
+                t = time.strptime(req.args.get('from'), '%x')
+            except:
+                pass
+
+        fromdate = time.mktime((t[0], t[1], t[2], 23, 59, 59, t[6], t[7], t[8]))
+        try:
+            daysback = max(0, int(req.args.get('daysback', '')))
+        except ValueError:
+            daysback = self.default_daysback
+        req.hdf['timeline.from'] = format_date(fromdate)
+        req.hdf['timeline.daysback'] = daysback
+
+        available_filters = []
+        for event_provider in self.event_providers:
+            available_filters += event_provider.get_timeline_filters(req)
+
+        filters = []
+        # check the request or session for enabled filters, or use default
+        for test in (lambda f: req.args.has_key(f[0]),
+                     lambda f: req.session.get('timeline.filter.%s' % f[0], '')\
+                               == '1',
+                     lambda f: len(f) == 2 or f[2]):
+            if filters:
+                break
+            filters = [f[0] for f in available_filters if test(f)]
+
+        # save the results of submitting the timeline form to the session
+        if req.args.has_key('update'):
+            for filter in available_filters:
+                key = 'timeline.filter.%s' % filter[0]
+                if req.args.has_key(filter[0]):
+                    req.session[key] = '1'
+                elif req.session.has_key(key):
+                    del req.session[key]
+
+        stop = fromdate
+        start = stop - (daysback + 1) * 86400
+
+        events = []
+        for event_provider in self.event_providers:
+            try:
+                events += event_provider.get_timeline_events(req, start, stop,
+                                                             filters)
+            except Exception, e: # cope with a failure of that provider
+                self._provider_failure(e, req, event_provider, filters,
+                                       [f[0] for f in available_filters])
+
+        events.sort(lambda x,y: cmp(y[3], x[3]))
+        if maxrows and len(events) > maxrows:
+            del events[maxrows:]
+
+        req.hdf['title'] = 'Timeline'
+
+        # Get the email addresses of all known users
+        email_map = {}
+        for username, name, email in self.env.get_known_users():
+            if email:
+                email_map[username] = email
+
+        idx = 0
+        for kind, href, title, date, author, message in events:
+            event = {'kind': kind, 'title': title, 'href': href,
+                     'author': author or 'anonymous',
+                     'date': format_date(date),
+                     'time': format_time(date, '%H:%M'),
+                     'message': message}
+
+            if format == 'rss':
+                # Strip/escape HTML markup
+                if isinstance(title, Markup):
+                    title = title.plaintext(keeplinebreaks=False)
+                event['title'] = title
+                event['message'] = to_unicode(message)
+
+                if author:
+                    # For RSS, author must be an email address
+                    if author.find('@') != -1:
+                        event['author.email'] = author
+                    elif email_map.has_key(author):
+                        event['author.email'] = email_map[author]
+                event['date'] = http_date(date)
+
+            req.hdf['timeline.events.%s' % idx] = event
+            idx += 1
+
+        if format == 'rss':
+            return 'timeline_rss.cs', 'application/rss+xml'
+
+        add_stylesheet(req, 'common/css/timeline.css')
+        rss_href = req.href.timeline([(f, 'on') for f in filters],
+                                     daysback=90, max=50, format='rss')
+        add_link(req, 'alternate', rss_href, 'RSS Feed', 'application/rss+xml',
+                 'rss')
+        for idx,fltr in enumerate(available_filters):
+            req.hdf['timeline.filters.%d' % idx] = {'name': fltr[0],
+                'label': fltr[1], 'enabled': int(fltr[0] in filters)}
+
+        return 'timeline.cs', None
+
+    def _provider_failure(self, exc, req, ep, current_filters, all_filters):
+        """Raise a TracError exception explaining the failure of a provider.
+
+        At the same time, the message will contain a link to the timeline
+        without the filters corresponding to the guilty event provider `ep`.
+        """
+        ep_name, exc_name = [i.__class__.__name__ for i in (ep, exc)]
+        guilty_filters = [f[0] for f in ep.get_timeline_filters(req)]
+        guilty_kinds = [f[1] for f in ep.get_timeline_filters(req)]
+        other_filters = [f for f in current_filters if not f in guilty_filters]
+        if not other_filters:
+            other_filters = [f for f in all_filters if not f in guilty_filters]
+        args = [(a, req.args.get(a)) for a in ('from', 'format', 'max',
+                                               'daysback')]
+        href = req.href.timeline(args+[(f, 'on') for f in other_filters])
+        raise TracError(Markup(
+            '%s  event provider (<tt>%s</tt>) failed:<br /><br />'
+            '%s: %s'
+            '<p>You may want to see the other kind of events from the '
+            '<a href="%s">Timeline</a></p>', 
+            ", ".join(guilty_kinds), ep_name, exc_name, to_unicode(exc), href))
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/__init__.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+
+"""
+Trac
+Edgewall Software
+
+U{http://trac.edgewall.com/}
+
+@author: Jonas Borgström <jonas@edgewall.com>
+@author: Daniel Lundin <daniel@edgewall.com>
+"""
+__docformat__ = 'epytext en'
+
+__version__ = '0.10dev'
+__url__ = 'http://trac.edgewall.com/'
+__copyright__ = '(C) 2003-2006 Edgewall Software'
+__license__ = 'BSD'
+__license_long__ = """
+ Copyright (C) 2003-2006 Edgewall Software
+ All rights reserved.
+ 
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 
+  1. Redistributions of source code must retain the above copyright
+     notice, this list of conditions and the following disclaimer.
+  2. Redistributions in binary form must reproduce the above copyright
+     notice, this list of conditions and the following disclaimer in
+     the documentation and/or other materials provided with the
+     distribution.
+  3. The name of the author may not be used to endorse or promote
+     products derived from this software without specific prior
+     written permission.
+ 
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+ GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
+ IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+ IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."""
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/attachment.py
@@ -0,0 +1,605 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+import os
+import re
+import shutil
+import time
+import unicodedata
+
+from trac import perm, util
+from trac.config import BoolOption, IntOption
+from trac.core import *
+from trac.env import IEnvironmentSetupParticipant
+from trac.mimeview import *
+from trac.util import get_reporter_id, create_unique_file
+from trac.util.datefmt import format_datetime, pretty_timedelta
+from trac.util.markup import Markup, html
+from trac.util.text import unicode_quote, unicode_unquote, pretty_size
+from trac.web import HTTPBadRequest, IRequestHandler
+from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
+from trac.wiki.api import IWikiSyntaxProvider
+from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner
+
+
+class InvalidAttachment(TracError):
+    """Exception raised when attachment validation fails."""
+
+
+class IAttachmentChangeListener(Interface):
+    """Extension point interface for components that require notification when
+    attachments are created or deleted."""
+    def attachment_added(attachment):
+        """Called when an attachment is added."""
+
+    def attachment_deleted(attachment):
+        """Called when an attachment is deleted."""
+
+
+class IAttachmentManipulator(Interface):
+    """Extension point interface for components that need to manipulate
+    attachments.
+    
+    Unlike change listeners, a manipulator can reject changes being committed
+    to the database."""
+    def prepare_attachment(req, attachment, fields):
+        """Not currently called, but should be provided for future
+        compatibility."""
+
+    def validate_attachment(req, attachment):
+        """Validate an attachment after upload but before being stored in Trac
+        environment.
+        
+        Must return a list of `(field, message)` tuples, one for each problem
+        detected. `field` can be any of `description`, `username`, `filename`,
+        `content`, or `None` to indicate an overall problem with the
+        attachment. Therefore, a return value of `[]` means everything is
+        OK."""
+
+
+class Attachment(object):
+
+    def __init__(self, env, parent_type, parent_id, filename=None, db=None):
+        self.env = env
+        self.parent_type = parent_type
+        self.parent_id = unicode(parent_id)
+        if filename:
+            self._fetch(filename, db)
+        else:
+            self.filename = None
+            self.description = None
+            self.size = None
+            self.time = None
+            self.author = None
+            self.ipnr = None
+
+    def _fetch(self, filename, db=None):
+        if not db:
+            db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT filename,description,size,time,author,ipnr "
+                       "FROM attachment WHERE type=%s AND id=%s "
+                       "AND filename=%s ORDER BY time",
+                       (self.parent_type, unicode(self.parent_id), filename))
+        row = cursor.fetchone()
+        cursor.close()
+        if not row:
+            self.filename = filename
+            raise TracError('Attachment %s does not exist.' % (self.title),
+                            'Invalid Attachment')
+        self.filename = row[0]
+        self.description = row[1]
+        self.size = row[2] and int(row[2]) or 0
+        self.time = row[3] and int(row[3]) or 0
+        self.author = row[4]
+        self.ipnr = row[5]
+
+    def _get_path(self):
+        path = os.path.join(self.env.path, 'attachments', self.parent_type,
+                            unicode_quote(self.parent_id))
+        if self.filename:
+            path = os.path.join(path, unicode_quote(self.filename))
+        return os.path.normpath(path)
+    path = property(_get_path)
+
+    def href(self, req, *args, **dict):
+        return req.href.attachment(self.parent_type, self.parent_id,
+                                   self.filename, *args, **dict)
+
+    def parent_href(self, req):
+        return req.href(self.parent_type, self.parent_id)
+
+    def _get_title(self):
+        return '%s%s: %s' % (self.parent_type == 'ticket' and '#' or '',
+                             self.parent_id, self.filename)
+    title = property(_get_title)
+
+    def delete(self, db=None):
+        assert self.filename, 'Cannot delete non-existent attachment'
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        cursor.execute("DELETE FROM attachment WHERE type=%s AND id=%s "
+                       "AND filename=%s", (self.parent_type, self.parent_id,
+                       self.filename))
+        if os.path.isfile(self.path):
+            try:
+                os.unlink(self.path)
+            except OSError:
+                self.env.log.error('Failed to delete attachment file %s',
+                                   self.path, exc_info=True)
+                if handle_ta:
+                    db.rollback()
+                raise TracError, 'Could not delete attachment'
+
+        self.env.log.info('Attachment removed: %s' % self.title)
+        if handle_ta:
+            db.commit()
+
+        for listener in AttachmentModule(self.env).change_listeners:
+            listener.attachment_deleted(self)
+
+
+    def insert(self, filename, fileobj, size, t=None, db=None):
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        self.size = size
+        self.time = t or time.time()
+
+        # Make sure the path to the attachment is inside the environment
+        # attachments directory
+        attachments_dir = os.path.join(os.path.normpath(self.env.path),
+                                       'attachments')
+        commonprefix = os.path.commonprefix([attachments_dir, self.path])
+        assert commonprefix == attachments_dir
+
+        if not os.access(self.path, os.F_OK):
+            os.makedirs(self.path)
+        filename = unicode_quote(filename)
+        path, targetfile = create_unique_file(os.path.join(self.path,
+                                                           filename))
+        try:
+            # Note: `path` is an unicode string because `self.path` was one.
+            # As it contains only quoted chars and numbers, we can use `ascii`
+            basename = os.path.basename(path).encode('ascii')
+            filename = unicode_unquote(basename)
+
+            cursor = db.cursor()
+            cursor.execute("INSERT INTO attachment "
+                           "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)",
+                           (self.parent_type, self.parent_id, filename,
+                            self.size, self.time, self.description, self.author,
+                            self.ipnr))
+            shutil.copyfileobj(fileobj, targetfile)
+            self.filename = filename
+
+            self.env.log.info('New attachment: %s by %s', self.title,
+                              self.author)
+
+            if handle_ta:
+                db.commit()
+
+            for listener in AttachmentModule(self.env).change_listeners:
+                listener.attachment_added(self)
+
+        finally:
+            targetfile.close()
+
+    def select(cls, env, parent_type, parent_id, db=None):
+        if not db:
+            db = env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT filename,description,size,time,author,ipnr "
+                       "FROM attachment WHERE type=%s AND id=%s ORDER BY time",
+                       (parent_type, unicode(parent_id)))
+        for filename,description,size,time,author,ipnr in cursor:
+            attachment = Attachment(env, parent_type, parent_id)
+            attachment.filename = filename
+            attachment.description = description
+            attachment.size = size
+            attachment.time = time
+            attachment.author = author
+            attachment.ipnr = ipnr
+            yield attachment
+
+    select = classmethod(select)
+
+    def open(self):
+        self.env.log.debug('Trying to open attachment at %s', self.path)
+        try:
+            fd = open(self.path, 'rb')
+        except IOError:
+            raise TracError('Attachment %s not found' % self.filename)
+        return fd
+
+
+# Templating utilities
+
+def attachments_to_hdf(env, req, db, parent_type, parent_id):
+    return [attachment_to_hdf(env, req, db, attachment) for attachment
+            in Attachment.select(env, parent_type, parent_id, db)]
+    
+def attachment_to_hdf(env, req, db, attachment):
+    if not db:
+        db = env.get_db_cnx()
+    hdf = {
+        'filename': attachment.filename,
+        'description': wiki_to_oneliner(attachment.description, env, db),
+        'author': attachment.author,
+        'ipnr': attachment.ipnr,
+        'size': pretty_size(attachment.size),
+        'time': format_datetime(attachment.time),
+        'age': pretty_timedelta(attachment.time),
+        'href': attachment.href(req)
+    }
+    return hdf
+
+
+class AttachmentModule(Component):
+
+    implements(IEnvironmentSetupParticipant, IRequestHandler,
+               INavigationContributor, IWikiSyntaxProvider)
+
+    change_listeners = ExtensionPoint(IAttachmentChangeListener)
+    manipulators = ExtensionPoint(IAttachmentManipulator)
+
+    CHUNK_SIZE = 4096
+
+    max_size = IntOption('attachment', 'max_size', 262144,
+        """Maximum allowed file size for ticket and wiki attachments.""")
+
+    render_unsafe_content = BoolOption('attachment', 'render_unsafe_content',
+                                       'false',
+        """Whether non-binary attachments should be rendered in the browser, or
+        only made downloadable.
+
+        Pretty much any text file may be interpreted as HTML by the browser,
+        which allows a malicious user to attach a file containing cross-site
+        scripting attacks.
+
+        For public sites where anonymous users can create attachments, it is
+        recommended to leave this option disabled (which is the default).""")
+
+    # IEnvironmentSetupParticipant methods
+
+    def environment_created(self):
+        """Create the attachments directory."""
+        if self.env.path:
+            os.mkdir(os.path.join(self.env.path, 'attachments'))
+
+    def environment_needs_upgrade(self, db):
+        return False
+
+    def upgrade_environment(self, db):
+        pass
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return req.args.get('type')
+
+    def get_navigation_items(self, req):
+        return []
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        match = re.match(r'^/attachment/(ticket|wiki)(?:[/:](.*))?$',
+                         req.path_info)
+        if match:
+            req.args['type'] = match.group(1)
+            req.args['path'] = match.group(2).replace(':', '/')
+            return True
+
+    def process_request(self, req):
+        parent_type = req.args.get('type')
+        path = req.args.get('path')
+        if not parent_type or not path:
+            raise HTTPBadRequest('Bad request')
+        if not parent_type in ['ticket', 'wiki']:
+            raise HTTPBadRequest('Unknown attachment type')
+
+        action = req.args.get('action', 'view')
+        if action == 'new':
+            attachment = Attachment(self.env, parent_type, path)
+        else:
+            segments = path.split('/')
+            parent_id = '/'.join(segments[:-1])
+            last_segment = segments[-1]
+            if len(segments) == 1:
+                self._render_list(req, parent_type, last_segment)
+                return 'attachment.cs', None
+            if not last_segment:
+                raise HTTPBadRequest('Bad request')
+            attachment = Attachment(self.env, parent_type, parent_id,
+                                    last_segment)
+        parent_link, parent_text = self._parent_to_hdf(
+            req, attachment.parent_type, attachment.parent_id)
+        if req.method == 'POST':
+            if action == 'new':
+                self._do_save(req, attachment)
+            elif action == 'delete':
+                self._do_delete(req, attachment)
+        elif action == 'delete':
+            self._render_confirm(req, attachment)
+        elif action == 'new':
+            self._render_form(req, attachment)
+        else:
+            add_link(req, 'up', parent_link, parent_text)
+            self._render_view(req, attachment)
+
+        add_stylesheet(req, 'common/css/code.css')
+        return 'attachment.cs', None
+
+    def _parent_to_hdf(self, req, parent_type, parent_id):
+        # Populate attachment.parent:
+        parent_link = req.href(parent_type, parent_id)
+        if parent_type == 'ticket':
+            parent_text = 'Ticket #' + parent_id
+        else: # 'wiki'
+            parent_text = parent_id
+        req.hdf['attachment.parent'] = {
+            'type': parent_type, 'id': parent_id,
+            'name': parent_text, 'href': parent_link
+        }
+        return parent_link, parent_text
+
+    # IWikiSyntaxProvider methods
+    
+    def get_wiki_syntax(self):
+        return []
+
+    def get_link_resolvers(self):
+        yield ('attachment', self._format_link)
+
+    # Public methods
+
+    def get_history(self, start, stop, type):
+        """Return an iterable of tuples describing changes to attachments on
+        a particular object type.
+
+        The tuples are in the form (change, type, id, filename, time,
+        description, author). `change` can currently only be `created`."""
+        # Traverse attachment directory
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT type, id, filename, time, description, author "
+                       "  FROM attachment "
+                       "  WHERE time > %s AND time < %s "
+                       "        AND type = %s", (start, stop, type))
+        for type, id, filename, time, description, author in cursor:
+            yield ('created', type, id, filename, time, description, author)
+
+    def get_timeline_events(self, req, db, type, format, start, stop, display):
+        """Return an iterable of events suitable for ITimelineEventProvider.
+
+        `display` is a callback for formatting the attachment's parent
+        """
+        for change, type, id, filename, time, descr, author in \
+                self.get_history(start, stop, type):
+            title = html.EM(os.path.basename(filename)) + \
+                    ' attached to ' + display(id)
+            if format == 'rss':
+                descr = wiki_to_html(descr or '--', self.env, req, db,
+                                     absurls=True)
+                href = req.abs_href
+            else:
+                descr = wiki_to_oneliner(descr, self.env, db, shorten=True)
+                title += Markup(' by %s', author)
+                href = req.href
+            yield('attachment', href.attachment(type, id, filename), title,
+                  time, author, descr)
+
+    # Internal methods
+
+    def _do_save(self, req, attachment):
+        perm_map = {'ticket': 'TICKET_APPEND', 'wiki': 'WIKI_MODIFY'}
+        req.perm.assert_permission(perm_map[attachment.parent_type])
+
+        if req.args.has_key('cancel'):
+            req.redirect(attachment.parent_href(req))
+
+        upload = req.args['attachment']
+        if not hasattr(upload, 'filename') or not upload.filename:
+            raise TracError('No file uploaded')
+        if hasattr(upload.file, 'fileno'):
+            size = os.fstat(upload.file.fileno())[6]
+        else:
+            size = upload.file.len
+        if size == 0:
+            raise TracError("Can't upload empty file")
+
+        # Maximum attachment size (in bytes)
+        max_size = self.max_size
+        if max_size >= 0 and size > max_size:
+            raise TracError('Maximum attachment size: %d bytes' % max_size,
+                            'Upload failed')
+
+        # We try to normalize the filename to unicode NFC if we can.
+        # Files uploaded from OS X might be in NFD.
+        filename = unicodedata.normalize('NFC', unicode(upload.filename,
+                                                        'utf-8'))
+        filename = filename.replace('\\', '/').replace(':', '/')
+        filename = os.path.basename(filename)
+        if not filename:
+            raise TracError('No file uploaded')
+
+        attachment.description = req.args.get('description', '')
+        attachment.author = get_reporter_id(req, 'author')
+        attachment.ipnr = req.remote_addr
+
+        # Validate attachment
+        for manipulator in self.manipulators:
+            for field, message in manipulator.validate_attachment(req, attachment):
+                if field:
+                    raise InvalidAttachment('Attachment field %s is invalid: %s'
+                                            % (field, message))
+                else:
+                    raise InvalidAttachment('Invalid attachment: %s' % message)
+
+        if req.args.get('replace'):
+            try:
+                old_attachment = Attachment(self.env, attachment.parent_type,
+                                            attachment.parent_id, filename)
+                if not (old_attachment.author and req.authname \
+                        and old_attachment.author == req.authname):
+                    perm_map = {'ticket': 'TICKET_ADMIN', 'wiki': 'WIKI_DELETE'}
+                    req.perm.assert_permission(perm_map[old_attachment.parent_type])
+                old_attachment.delete()
+            except TracError:
+                pass # don't worry if there's nothing to replace
+            attachment.filename = None
+        attachment.insert(filename, upload.file, size)
+
+        # Redirect the user to the newly created attachment
+        req.redirect(attachment.href(req))
+
+    def _do_delete(self, req, attachment):
+        perm_map = {'ticket': 'TICKET_ADMIN', 'wiki': 'WIKI_DELETE'}
+        req.perm.assert_permission(perm_map[attachment.parent_type])
+
+        if req.args.has_key('cancel'):
+            req.redirect(attachment.href(req))
+
+        attachment.delete()
+
+        # Redirect the user to the attachment parent page
+        req.redirect(attachment.parent_href(req))
+
+    def _render_confirm(self, req, attachment):
+        perm_map = {'ticket': 'TICKET_ADMIN', 'wiki': 'WIKI_DELETE'}
+        req.perm.assert_permission(perm_map[attachment.parent_type])
+
+        req.hdf['title'] = '%s (delete)' % attachment.title
+        req.hdf['attachment'] = {'filename': attachment.filename,
+                                 'mode': 'delete'}
+
+    def _render_form(self, req, attachment):
+        perm_map = {'ticket': 'TICKET_APPEND', 'wiki': 'WIKI_MODIFY'}
+        req.perm.assert_permission(perm_map[attachment.parent_type])
+
+        req.hdf['attachment'] = {'mode': 'new',
+                                 'author': get_reporter_id(req)}
+
+    def _render_view(self, req, attachment):
+        perm_map = {'ticket': 'TICKET_VIEW', 'wiki': 'WIKI_VIEW'}
+        req.perm.assert_permission(perm_map[attachment.parent_type])
+
+        req.check_modified(attachment.time)
+
+        # Render HTML view
+        req.hdf['title'] = attachment.title
+        req.hdf['attachment'] = attachment_to_hdf(self.env, req, None,
+                                                  attachment)
+        # Override the 'oneliner'
+        req.hdf['attachment.description'] = wiki_to_html(attachment.description,
+                                                         self.env, req)
+
+        perm_map = {'ticket': 'TICKET_ADMIN', 'wiki': 'WIKI_DELETE'}
+        if req.perm.has_permission(perm_map[attachment.parent_type]):
+            req.hdf['attachment.can_delete'] = 1
+
+        fd = attachment.open()
+        try:
+            mimeview = Mimeview(self.env)
+
+            # MIME type detection
+            str_data = fd.read(1000)
+            fd.seek(0)
+            
+            binary = is_binary(str_data)
+            mime_type = mimeview.get_mimetype(attachment.filename, str_data)
+
+            # Eventually send the file directly
+            format = req.args.get('format')
+            if format in ('raw', 'txt'):
+                if not self.render_unsafe_content and not binary:
+                    # Force browser to download HTML/SVG/etc pages that may
+                    # contain malicious code enabling XSS attacks
+                    req.send_header('Content-Disposition', 'attachment;' +
+                                    'filename=' + attachment.filename)
+                if not mime_type or (self.render_unsafe_content and \
+                                     not binary and format == 'txt'):
+                    mime_type = 'text/plain'
+                if 'charset=' not in mime_type:
+                    charset = mimeview.get_charset(str_data, mime_type)
+                    mime_type = mime_type + '; charset=' + charset
+                req.send_file(attachment.path, mime_type)
+
+            # add ''Plain Text'' alternate link if needed
+            if self.render_unsafe_content and not binary and \
+               mime_type and not mime_type.startswith('text/plain'):
+                plaintext_href = attachment.href(req, format='txt')
+                add_link(req, 'alternate', plaintext_href, 'Plain Text',
+                         mime_type)
+
+            # add ''Original Format'' alternate link (always)
+            raw_href = attachment.href(req, format='raw')
+            add_link(req, 'alternate', raw_href, 'Original Format', mime_type)
+
+            self.log.debug("Rendering preview of file %s with mime-type %s"
+                           % (attachment.filename, mime_type))
+
+            req.hdf['attachment'] = mimeview.preview_to_hdf(
+                req, fd, os.fstat(fd.fileno()).st_size, mime_type,
+                attachment.filename, raw_href, annotations=['lineno'])
+        finally:
+            fd.close()
+
+    def _render_list(self, req, p_type, p_id):
+        self._parent_to_hdf(req, p_type, p_id)
+        req.hdf['attachment'] = {
+            'mode': 'list',
+            'list': attachments_to_hdf(self.env, req, None, p_type, p_id),
+            'attach_href': req.href.attachment(p_type, p_id)
+            }
+
+    def _format_link(self, formatter, ns, target, label):
+        link, params, fragment = formatter.split_link(target)
+        ids = link.split(':', 2)
+        if len(ids) == 3:
+            parent_type, parent_id, filename = ids
+        else:
+            # FIXME: the formatter should know which object the text being
+            #        formatter belongs to
+            parent_type, parent_id = 'wiki', 'WikiStart'
+            if formatter.req:
+                path_info = formatter.req.path_info.split('/', 2)
+                if len(path_info) > 1:
+                    parent_type = path_info[1]
+                if len(path_info) > 2:
+                    parent_id = path_info[2]
+            filename = link
+        href = formatter.href()
+        try:
+            attachment = Attachment(self.env, parent_type, parent_id, filename)
+            if formatter.req:
+                href = attachment.href(formatter.req) + params
+            return html.A(label, class_='attachment', href=href,
+                          title='Attachment %s' % attachment.title)
+        except TracError:
+            return html.A(label, class_='missing attachment', rel='nofollow',
+                          href=formatter.href())
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/config.py
@@ -0,0 +1,415 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2006 Edgewall Software
+# Copyright (C) 2005-2006 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+from ConfigParser import ConfigParser
+import os
+try:
+    set
+except NameError:
+    from sets import Set as set
+import sys
+
+from trac.core import ExtensionPoint, TracError
+from trac.util import sorted, to_unicode
+
+__all__ = ['Configuration', 'Option', 'BoolOption', 'IntOption', 'ListOption',
+           'ExtensionOption', 'OrderedExtensionsOption', 'ConfigurationError',
+           'default_dir']
+
+_TRUE_VALUES = ('yes', 'true', 'on', 'aye', '1', 1, True)
+
+
+class ConfigurationError(TracError):
+    """Exception raised when a value in the configuration file is not valid."""
+
+
+class Configuration(object):
+    """Thin layer over `ConfigParser` from the Python standard library.
+
+    In addition to providing some convenience methods, the class remembers
+    the last modification time of the configuration file, and reparses it
+    when the file has changed.
+    """
+    def __init__(self, filename):
+        self._sections = {}
+        self.filename = filename
+        self.parser = ConfigParser()
+        self._lastmtime = 0
+        self.site_filename = os.path.join(default_dir('conf'), 'trac.ini')
+        self.site_parser = ConfigParser()
+        self._lastsitemtime = 0
+        self.parse_if_needed()
+
+    def __contains__(self, name):
+        """Return whether the configuration contains a section of the given
+        name.
+        """
+        return self.parser.has_section(name)
+
+    def __getitem__(self, name):
+        """Return the configuration section with the specified name."""
+        if name not in self._sections:
+            self._sections[name] = Section(self, name)
+        return self._sections[name]
+
+    def get(self, section, name, default=None):
+        """Return the value of the specified option."""
+        return self[section].get(name, default)
+
+    def getbool(self, section, name, default=None):
+        """Return the specified option as boolean value.
+        
+        If the value of the option is one of "yes", "true",  "on", or "1", this
+        method wll return `True`, otherwise `False`.
+        
+        (since Trac 0.9.3)
+        """
+        return self[section].getbool(name, default)
+
+    def getint(self, section, name, default=None):
+        """Return the value of the specified option as integer.
+        
+        If the specified option can not be converted to an integer, a
+        `ConfigurationError` exception is raised.
+        
+        (since Trac 0.10)
+        """
+        return self[section].getint(name, default)
+
+    def getlist(self, section, name, default=None, sep=',', keep_empty=False):
+        """Return a list of values that have been specified as a single
+        comma-separated option.
+        
+        A different separator can be specified using the `sep` parameter. If
+        the `keep_empty` parameter is set to `True`, empty elements are
+        included in the list.
+        
+        (since Trac 0.10)
+        """
+        return self[section].getlist(name, default, sep, keep_empty)
+
+    def set(self, section, name, value):
+        """Change a configuration value.
+        
+        These changes are not persistent unless saved with `save()`.
+        """
+        self[section].set(name, value)
+
+    def defaults(self):
+        """Returns a dictionary of the default configuration values.
+        
+        (since Trac 0.10)
+        """
+        defaults = {}
+        for (section, name), option in Option.registry.items():
+            defaults.setdefault(section, {})[name] = option.default
+        return defaults
+
+    def options(self, section):
+        """Return a list of `(name, value)` tuples for every option in the
+        specified section.
+        
+        This includes options that have default values that haven't been
+        overridden.
+        """
+        return self[section].options()
+
+    def remove(self, section, name):
+        """Remove the specified option."""
+        if self.parser.has_section(section):
+            self.parser.remove_option(section, name)
+
+    def sections(self):
+        """Return a list of section names."""
+        return sorted(set(self.site_parser.sections() + self.parser.sections()))
+
+    def save(self):
+        """Write the configuration options to the primary file."""
+        if not self.filename:
+            return
+
+        # Only save options that differ from the defaults
+        config = ConfigParser()
+        for section in self.sections():
+            for option in self[section]:
+                default = self.site_parser.has_option(section, option) and \
+                          self.site_parser.get(section, option)
+                current = self.parser.has_option(section, option) and \
+                          self.parser.get(section, option)
+                if current is not False and current != default:
+                    if not config.has_section(section):
+                        config.add_section(section)
+                    config.set(section, option, current or '')
+
+        fileobj = file(self.filename, 'w')
+        try:
+            config.write(fileobj)
+        finally:
+            fileobj.close()
+
+    def parse_if_needed(self):
+        # Load global configuration
+        if os.path.isfile(self.site_filename):
+            modtime = os.path.getmtime(self.site_filename)
+            if modtime > self._lastsitemtime:
+                self.site_parser.read(self.site_filename)
+                self._lastsitemtime = modtime
+
+        if not self.filename or not os.path.isfile(self.filename):
+            return
+        modtime = os.path.getmtime(self.filename)
+        if modtime > self._lastmtime:
+            self.parser.read(self.filename)
+            self._lastmtime = modtime
+
+
+class Section(object):
+    """Proxy for a specific configuration section.
+    
+    Objects of this class should not be instantiated directly.
+    """
+    __slots__ = ['config', 'name']
+
+    def __init__(self, config, name):
+        self.config = config
+        self.name = name
+
+    def __contains__(self, name):
+        return self.config.parser.has_option(self.name, name) or \
+               self.config.site_parser.has_option(self.name, name) 
+
+    def __iter__(self):
+        options = []
+        if self.config.parser.has_section(self.name):
+            for option in self.config.parser.options(self.name):
+                options.append(option.lower())
+                yield option
+        if self.config.site_parser.has_section(self.name):
+            for option in self.config.site_parser.options(self.name):
+                if option.lower() not in options:
+                    yield option
+
+    def __repr__(self):
+        return '<Section [%s]>' % (self.name)
+
+    def get(self, name, default=None):
+        """Return the value of the specified option."""
+        if self.config.parser.has_option(self.name, name):
+            value = self.config.parser.get(self.name, name)
+        elif self.config.site_parser.has_option(self.name, name):
+            value = self.config.site_parser.get(self.name, name)
+        else:
+            option = Option.registry.get((self.name, name))
+            if option:
+                value = option.default or default
+            else:
+                value = default
+        if value is None:
+            return ''
+        return to_unicode(value)
+
+    def getbool(self, name, default=None):
+        """Return the value of the specified option as boolean.
+        
+        This method returns `True` if the option value is one of "yes", "true",
+        "on", or "1", ignoring case. Otherwise `False` is returned.
+        """
+        value = self.get(name, default)
+        if isinstance(value, basestring):
+            value = value.lower() in _TRUE_VALUES
+        return bool(value)
+
+    def getint(self, name, default=None):
+        """Return the value of the specified option as integer.
+        
+        If the specified option can not be converted to an integer, a
+        `ConfigurationError` exception is raised.
+        """
+        value = self.get(name, default)
+        try:
+            return int(value)
+        except ValueError:
+            raise ConfigurationError('expected integer, got %s' % repr(value))
+
+    def getlist(self, name, default=None, sep=',', keep_empty=True):
+        """Return a list of values that have been specified as a single
+        comma-separated option.
+        
+        A different separator can be specified using the `sep` parameter. If
+        the `skip_empty` parameter is set to `True`, empty elements are omitted
+        from the list.
+        """
+        value = self.get(name, default)
+        if isinstance(value, basestring):
+            items = [item.strip() for item in value.split(sep)]
+        else:
+            items = list(value)
+        if not keep_empty:
+            items = filter(None, items)
+        return items
+
+    def options(self):
+        """Return `(name, value)` tuples for every option in the section."""
+        for name in self:
+            yield name, self.get(name)
+
+    def set(self, name, value):
+        """Change a configuration value.
+        
+        These changes are not persistent unless saved with `save()`.
+        """
+        if not self.config.parser.has_section(self.name):
+            self.config.parser.add_section(self.name)
+        return self.config.parser.set(self.name, name,
+                                      to_unicode(value).encode('utf-8'))
+
+
+class Option(object):
+    """Descriptor for configuration options on `Configurable` subclasses."""
+
+    registry = {}
+    accessor = Section.get
+
+    def __init__(self, section, name, default=None, doc=''):
+        """Create the extension point.
+        
+        @param section: the name of the configuration section this option
+            belongs to
+        @param name: the name of the option
+        @param default: the default value for the option
+        @param doc: documentation of the option
+        """
+        self.section = section
+        self.name = name
+        self.default = default
+        self.registry[(self.section, self.name)] = self
+        self.__doc__ = doc
+
+    def __get__(self, instance, owner):
+        if instance is None:
+            return self
+        config = getattr(instance, 'config', None)
+        if config and isinstance(config, Configuration):
+            section = config[self.section]
+            value = self.accessor(section, self.name, self.default)
+            return value
+        return None
+
+    def __set__(self, instance, value):
+        raise AttributeError, 'can\'t set attribute'
+
+    def __repr__(self):
+        return '<%s [%s] "%s">' % (self.__class__.__name__, self.section,
+                                   self.name)
+
+
+class BoolOption(Option):
+    """Descriptor for boolean configuration options."""
+    accessor = Section.getbool
+
+
+class IntOption(Option):
+    """Descriptor for integer configuration options."""
+    accessor = Section.getint
+
+
+class ListOption(Option):
+    """Descriptor for configuration options that contain multiple values
+    separated by a specific character."""
+
+    def __init__(self, section, name, default=None, sep=',', keep_empty=False,
+                 doc=''):
+        Option.__init__(self, section, name, default, doc)
+        self.sep = sep
+        self.keep_empty = keep_empty
+
+    def accessor(self, section, name, default):
+        return section.getlist(name, default, self.sep, self.keep_empty)
+
+
+class ExtensionOption(Option):
+
+    def __init__(self, section, name, interface, default=None, doc=''):
+        Option.__init__(self, section, name, default, doc)
+        self.xtnpt = ExtensionPoint(interface)
+
+    def __get__(self, instance, owner):
+        if instance is None:
+            return self
+        value = Option.__get__(self, instance, owner)
+        for impl in self.xtnpt.extensions(instance):
+            if impl.__class__.__name__ == value:
+                return impl
+        raise AttributeError('Cannot find an implementation of the "%s" '
+                             'interface named "%s".  Please update the option '
+                             '%s.%s in trac.ini.'
+                             % (self.xtnpt.interface.__name__, value,
+                                self.section, self.name))
+
+
+class OrderedExtensionsOption(ListOption):
+    """A comma separated, ordered, list of components implementing `interface`.
+    Can be empty.
+
+    If `include_missing` is true (the default) all components implementing the
+    interface are returned, with those specified by the option ordered first."""
+
+    def __init__(self, section, name, interface, default=None,
+                 include_missing=True, doc=''):
+        ListOption.__init__(self, section, name, default, doc=doc)
+        self.xtnpt = ExtensionPoint(interface)
+        self.include_missing = include_missing
+
+    def __get__(self, instance, owner):
+        if instance is None:
+            return self
+        order = ListOption.__get__(self, instance, owner)
+        components = []
+        for impl in self.xtnpt.extensions(instance):
+            if self.include_missing or impl.__class__.__name__ in order:
+                components.append(impl)
+
+        def compare(x, y):
+            x, y = x.__class__.__name__, y.__class__.__name__
+            if x not in order:
+                return int(y in order)
+            if y not in order:
+                return -int(x in order)
+            return cmp(order.index(x), order.index(y))
+        components.sort(compare)
+        return components
+
+
+def default_dir(name):
+    try:
+        from trac import siteconfig
+        return getattr(siteconfig, '__default_%s_dir__' % name)
+    except ImportError:
+        # This is not a regular install with a generated siteconfig.py file,
+        # so try to figure out the directory based on common setups
+        special_dirs = {'wiki': 'wiki-default', 'macros': 'wiki-macros'}
+        dirname = special_dirs.get(name, name)
+
+        # First assume we're being executing directly form the source directory
+        import trac
+        path = os.path.join(os.path.split(os.path.dirname(trac.__file__))[0],
+                            dirname)
+        if not os.path.isdir(path):
+            # Not being executed from the source directory, so assume the
+            # default installation prefix
+            path = os.path.join(sys.prefix, 'share', 'trac', dirname)
+
+        return path
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/core.py
@@ -0,0 +1,204 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+__all__ = ['Component', 'ExtensionPoint', 'implements', 'Interface',
+           'TracError']
+
+
+class TracError(Exception):
+    """Exception base class for errors in Trac."""
+
+    def __init__(self, message, title=None, show_traceback=False):
+        Exception.__init__(self, message)
+        self.message = message
+        self.title = title
+        self.show_traceback = show_traceback
+
+
+class Interface(object):
+    """Marker base class for extension point interfaces."""
+
+
+class ExtensionPoint(property):
+    """Marker class for extension points in components."""
+
+    def __init__(self, interface):
+        """Create the extension point.
+        
+        @param interface: the `Interface` subclass that defines the protocol
+            for the extension point
+        """
+        property.__init__(self, self.extensions)
+        self.interface = interface
+        self.__doc__ = 'List of components that implement `%s`' % \
+                       self.interface.__name__
+
+    def extensions(self, component):
+        """Return a list of components that declare to implement the extension
+        point interface."""
+        extensions = ComponentMeta._registry.get(self.interface, [])
+        return filter(None, [component.compmgr[cls] for cls in extensions])
+
+    def __repr__(self):
+        """Return a textual representation of the extension point."""
+        return '<ExtensionPoint %s>' % self.interface.__name__
+
+
+class ComponentMeta(type):
+    """Meta class for components.
+    
+    Takes care of component and extension point registration.
+    """
+    _components = []
+    _registry = {}
+
+    def __new__(cls, name, bases, d):
+        """Create the component class."""
+
+        new_class = type.__new__(cls, name, bases, d)
+        if name == 'Component':
+            # Don't put the Component base class in the registry
+            return new_class
+
+        # Only override __init__ for Components not inheriting ComponentManager
+        if True not in [issubclass(x, ComponentManager) for x in bases]:
+            # Allow components to have a no-argument initializer so that
+            # they don't need to worry about accepting the component manager
+            # as argument and invoking the super-class initializer
+            init = d.get('__init__')
+            if not init:
+                # Because we're replacing the initializer, we need to make sure
+                # that any inherited initializers are also called.
+                for init in [b.__init__._original for b in new_class.mro()
+                             if issubclass(b, Component)
+                             and '__init__' in b.__dict__]:
+                    break
+            def maybe_init(self, compmgr, init=init, cls=new_class):
+                if cls not in compmgr.components:
+                    compmgr.components[cls] = self
+                    if init:
+                        init(self)
+            maybe_init._original = init
+            new_class.__init__ = maybe_init
+
+        if d.get('abstract'):
+            # Don't put abstract component classes in the registry
+            return new_class
+
+        ComponentMeta._components.append(new_class)
+        for interface in d.get('_implements', []):
+            ComponentMeta._registry.setdefault(interface, []).append(new_class)
+        for base in [base for base in bases if hasattr(base, '_implements')]:
+            for interface in base._implements:
+                ComponentMeta._registry.setdefault(interface, []).append(new_class)
+
+        return new_class
+
+
+def implements(*interfaces):
+    """
+    Can be used in the class definiton of `Component` subclasses to declare
+    the extension points that are extended.
+    """
+    import sys
+
+    frame = sys._getframe(1)
+    locals = frame.f_locals
+
+    # Some sanity checks
+    assert locals is not frame.f_globals and '__module__' in frame.f_locals, \
+           'implements() can only be used in a class definition'
+    assert not '_implements' in locals, \
+           'implements() can only be used once in a class definition'
+
+    locals['_implements'] = interfaces
+
+
+class Component(object):
+    """Base class for components.
+
+    Every component can declare what extension points it provides, as well as
+    what extension points of other components it extends.
+    """
+    __metaclass__ = ComponentMeta
+
+    def __new__(cls, *args, **kwargs):
+        """Return an existing instance of the component if it has already been
+        activated, otherwise create a new instance.
+        """
+        # If this component is also the component manager, just invoke that
+        if issubclass(cls, ComponentManager):
+            self = super(Component, cls).__new__(cls)
+            self.compmgr = self
+            return self
+
+        # The normal case where the component is not also the component manager
+        compmgr = args[0]
+        self = compmgr.components.get(cls)
+        if self is None:
+            self = super(Component, cls).__new__(cls)
+            self.compmgr = compmgr
+            compmgr.component_activated(self)
+        return self
+
+
+class ComponentManager(object):
+    """The component manager keeps a pool of active components."""
+
+    def __init__(self):
+        """Initialize the component manager."""
+        self.components = {}
+        self.enabled = {}
+        if isinstance(self, Component):
+            self.components[self.__class__] = self
+
+    def __contains__(self, cls):
+        """Return wether the given class is in the list of active components."""
+        return cls in self.components
+
+    def __getitem__(self, cls):
+        """Activate the component instance for the given class, or return the
+        existing the instance if the component has already been activated."""
+        if cls not in self.enabled:
+            self.enabled[cls] = self.is_component_enabled(cls)
+        if not self.enabled[cls]:
+            return None
+        component = self.components.get(cls)
+        if not component:
+            if cls not in ComponentMeta._components:
+                raise TracError, 'Component "%s" not registered' % cls.__name__
+            try:
+                component = cls(self)
+            except TypeError, e:
+                raise TracError, 'Unable to instantiate component %r (%s)' \
+                                 % (cls, e)
+        return component
+
+    def component_activated(self, component):
+        """Can be overridden by sub-classes so that special initialization for
+        components can be provided.
+        """
+
+    def is_component_enabled(self, cls):
+        """Can be overridden by sub-classes to veto the activation of a
+        component.
+
+        If this method returns False, the component with the given class will
+        not be available.
+        """
+        return True
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/db/__init__.py
@@ -0,0 +1,2 @@
+from trac.db.api import *
+from trac.db.schema import *
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/db/api.py
@@ -0,0 +1,163 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+import os
+import urllib
+
+from trac.config import Option
+from trac.core import *
+from trac.db.pool import ConnectionPool
+
+
+def get_column_names(cursor):
+    return cursor.description and \
+           [(isinstance(d[0], str) and [unicode(d[0], 'utf-8')] or [d[0]])[0]
+            for d in cursor.description] or []
+
+
+class IDatabaseConnector(Interface):
+    """Extension point interface for components that support the connection to
+    relational databases."""
+
+    def get_supported_schemes():
+        """Return the connection URL schemes supported by the connector, and
+        their relative priorities as an iterable of `(scheme, priority)` tuples.
+        """
+
+    def get_connection(**kwargs):
+        """Create a new connection to the database."""
+        
+    def init_db(**kwargs):
+        """Initialize the database."""
+
+    def to_sql(table):
+        """Return the DDL statements necessary to create the specified table,
+        including indices."""
+
+
+class DatabaseManager(Component):
+
+    connectors = ExtensionPoint(IDatabaseConnector)
+
+    connection_uri = Option('trac', 'database', 'sqlite:db/trac.db',
+        """Database connection
+        [wiki:TracEnvironment#DatabaseConnectionStrings string] for this
+        project""")
+
+    def __init__(self):
+        self._cnx_pool = None
+
+    def init_db(self):
+        connector, args = self._get_connector()
+        connector.init_db(**args)
+
+    def get_connection(self):
+        if not self._cnx_pool:
+            connector, args = self._get_connector()
+            self._cnx_pool = ConnectionPool(5, connector, **args)
+        return self._cnx_pool.get_cnx()
+
+    def shutdown(self):
+        if self._cnx_pool:
+            self._cnx_pool.shutdown()
+            self._cnx_pool = None
+
+    def _get_connector(self): ### FIXME: Make it public?
+        scheme, args = _parse_db_str(self.connection_uri)
+        candidates = {}
+        connector = None
+        for connector in self.connectors:
+            for scheme_, priority in connector.get_supported_schemes():
+                if scheme_ != scheme:
+                    continue
+                highest = candidates.get(scheme_, (None, 0))[1]
+                if priority > highest:
+                    candidates[scheme] = (connector, priority)
+            connector = candidates.get(scheme, [None])[0]
+        if not connector:
+            raise TracError, 'Unsupported database type "%s"' % scheme
+
+        if scheme == 'sqlite':
+            # Special case for SQLite to support a path relative to the
+            # environment directory
+            if args['path'] != ':memory:' and \
+                   not args['path'].startswith('/'):
+                args['path'] = os.path.join(self.env.path,
+                                            args['path'].lstrip('/'))
+
+        return connector, args
+
+
+def _parse_db_str(db_str):
+    scheme, rest = db_str.split(':', 1)
+
+    if not rest.startswith('/'):
+        if scheme == 'sqlite':
+            # Support for relative and in-memory SQLite connection strings
+            host = None
+            path = rest
+        else:
+            raise TracError, 'Database connection string %s must start with ' \
+                             'scheme:/' % db_str
+    else:
+        if rest.startswith('/') and not rest.startswith('//'):
+            host = None
+            rest = rest[1:]
+        elif rest.startswith('///'):
+            host = None
+            rest = rest[3:]
+        else:
+            rest = rest[2:]
+            if rest.find('/') == -1:
+                host = rest
+                rest = ''
+            else:
+                host, rest = rest.split('/', 1)
+        path = None
+
+    if host and host.find('@') != -1:
+        user, host = host.split('@', 1)
+        if user.find(':') != -1:
+            user, password = user.split(':', 1)
+        else:
+            password = None
+    else:
+        user = password = None
+    if host and host.find(':') != -1:
+        host, port = host.split(':')
+        port = int(port)
+    else:
+        port = None
+
+    if not path:
+        path = '/' + rest
+    if os.name == 'nt':
+        # Support local paths containing drive letters on Win32
+        if len(rest) > 1 and rest[1] == '|':
+            path = "%s:%s" % (rest[0], rest[2:])
+
+    params = {}
+    if path.find('?') != -1:
+        path, qs = path.split('?', 1)
+        qs = qs.split('&')
+        for param in qs:
+            name, value = param.split('=', 1)
+            value = urllib.unquote(value)
+            params[name] = value
+
+    args = zip(('user', 'password', 'host', 'port', 'path', 'params'),
+               (user, password, host, port, path, params))
+    return scheme, dict([(key, value) for key, value in args if value])
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/db/mysql_backend.py
@@ -0,0 +1,157 @@
+# -*- coding: iso8859-1 -*-
+#
+# Copyright (C) 2005-2006 Edgewall Software
+# Copyright (C) 2005-2006 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005 Jeff Weiss <trac@jeffweiss.org>
+# Copyright (C) 2006 Andres Salomon <dilinger@athenacr.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import re
+
+from trac.core import *
+from trac.db.api import IDatabaseConnector
+from trac.db.util import ConnectionWrapper
+
+_like_escape_re = re.compile(r'([/_%])')
+
+
+class MySQLConnector(Component):
+    """MySQL database support for version 4.1 and greater.
+    
+    Database urls should be of the form:
+        mysql://user[:password]@host[:port]/database
+    """
+
+    implements(IDatabaseConnector)
+
+    def get_supported_schemes(self):
+        return [('mysql', 1)]
+
+    def get_connection(self, path, user=None, password=None, host=None,
+                       port=None, params={}):
+        return MySQLConnection(path, user, password, host, port, params)
+
+    def init_db(self, path, user=None, password=None, host=None, port=None,
+                params={}):
+        cnx = self.get_connection(path, user, password, host, port, params)
+        cursor = cnx.cursor()
+        from trac.db_default import schema
+        for table in schema:
+            for stmt in self.to_sql(table):
+                self.env.log.debug(stmt)
+                cursor.execute(stmt)
+        cnx.commit()
+
+    def _collist(self, table, columns):
+        """Take a list of columns and impose limits on each so that indexing
+        works properly.
+        
+        Some Versions of MySQL limit each index prefix to 500 bytes total, with
+        a max of 255 bytes per column.
+        """
+        cols = []
+        limit = 500 / len(columns)
+        if limit > 255:
+            limit = 255
+        for c in columns:
+            name = '`%s`' % c
+            table_col = filter((lambda x: x.name == c), table.columns)
+            if len(table_col) == 1 and table_col[0].type.lower() == 'text':
+                name += '(%s)' % limit
+            # For non-text columns, we simply throw away the extra bytes.
+            # That could certainly be optimized better, but for now let's KISS.
+            cols.append(name)
+        return ','.join(cols)
+
+    def to_sql(self, table):
+        sql = ['CREATE TABLE %s (' % table.name]
+        coldefs = []
+        for column in table.columns:
+            ctype = column.type
+            if column.auto_increment:
+                ctype = 'INT UNSIGNED NOT NULL AUTO_INCREMENT'
+                # Override the column type, as a text field cannot
+                # use auto_increment.
+                column.type = 'int'
+            coldefs.append('    `%s` %s' % (column.name, ctype))
+        if len(table.key) > 0:
+            coldefs.append('    PRIMARY KEY (%s)' %
+                           self._collist(table, table.key))
+        sql.append(',\n'.join(coldefs) + '\n)')
+        yield '\n'.join(sql)
+
+        for index in table.indices:
+            yield 'CREATE INDEX %s_%s_idx ON %s (%s);' % (table.name,
+                  '_'.join(index.columns), table.name,
+                  self._collist(table, index.columns))
+
+
+class MySQLConnection(ConnectionWrapper):
+    """Connection wrapper for MySQL."""
+
+    poolable = True
+
+    def _mysqldb_gt_or_eq(self, v):
+        """This function checks whether the version of python-mysqldb
+        is greater than or equal to the version that's passed to it.
+        Note that the tuple only checks the major, minor, and sub versions;
+        the sub-sub version is weird, so we only check for 'final' versions.
+        """
+        from MySQLdb import version_info as ver
+        if ver[0] < v[0] or ver[1] < v[1] or ver[2] < v[2]:
+            return False
+        if ver[3] != 'final':
+            return False
+        return True
+
+    def _set_character_set(self, cnx, charset):
+        vers = tuple([ int(n) for n in cnx.get_server_info().split('.')[:2] ])
+        if vers < (4, 1):
+            raise TracError, 'MySQL servers older than 4.1 are not supported!'
+        cnx.query('SET NAMES %s' % charset)
+        cnx.store_result()
+        cnx.charset = charset
+
+    def __init__(self, path, user=None, password=None, host=None,
+                 port=None, params={}):
+        import MySQLdb
+
+        if path.startswith('/'):
+            path = path[1:]
+        if password == None:
+            password = ''
+        if port == None:
+            port = 3306
+
+        # python-mysqldb 1.2.1 added a 'charset' arg that is required for
+        # unicode stuff.  We hack around that here for older versions; at
+        # some point, this hack should be removed, and a strict requirement
+        # on 1.2.1 made.  -dilinger
+        if (self._mysqldb_gt_or_eq((1, 2, 1))):
+            cnx = MySQLdb.connect(db=path, user=user, passwd=password,
+                                  host=host, port=port, charset='utf8')
+        else:
+            cnx = MySQLdb.connect(db=path, user=user, passwd=password,
+                                  host=host, port=port, use_unicode=True)
+            self._set_character_set(cnx, 'utf8')
+        ConnectionWrapper.__init__(self, cnx)
+
+    def cast(self, column, type):
+        return 'CAST(%s AS %s)' % (column, type)
+
+    def like(self):
+        return "LIKE %s ESCAPE '/'"
+
+    def like_escape(self, text):
+        return _like_escape_re.sub(r'/\1', text)
+
+    def get_last_id(self, cursor, table, column='id'):
+        return self.cnx.insert_id()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/db/pool.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+try:
+    import threading
+except ImportError:
+    import dummy_threading as threading
+    threading._get_ident = lambda: 0
+import time
+
+from trac.db.util import ConnectionWrapper
+
+
+class TimeoutError(Exception):
+    """Exception raised by the connection pool when no connection has become
+    available after a given timeout."""
+
+
+class PooledConnection(ConnectionWrapper):
+    """A database connection that can be pooled. When closed, it gets returned
+    to the pool.
+    """
+
+    def __init__(self, pool, cnx):
+        ConnectionWrapper.__init__(self, cnx)
+        self._pool = pool
+
+    def close(self):
+        if self.cnx:
+            self._pool._return_cnx(self.cnx)
+            self.cnx = None
+
+    def __del__(self):
+        self.close()
+
+
+class ConnectionPool(object):
+    """A very simple connection pool implementation."""
+
+    def __init__(self, maxsize, connector, **kwargs):
+        self._dormant = [] # inactive connections in pool
+        self._active = {} # active connections by thread ID
+        self._available = threading.Condition(threading.Lock())
+        self._maxsize = maxsize # maximum pool size
+        self._cursize = 0 # current pool size, includes active connections
+        self._connector = connector
+        self._kwargs = kwargs
+
+    def get_cnx(self, timeout=None):
+        start = time.time()
+        self._available.acquire()
+        try:
+            tid = threading._get_ident()
+            if tid in self._active:
+                self._active[tid][0] += 1
+                return PooledConnection(self, self._active[tid][1])
+            while True:
+                if self._dormant:
+                    cnx = self._dormant.pop()
+                    break
+                elif self._maxsize and self._cursize < self._maxsize:
+                    cnx = self._connector.get_connection(**self._kwargs)
+                    self._cursize += 1
+                    break
+                else:
+                    if timeout:
+                        self._available.wait(timeout)
+                        if (time.time() - start) >= timeout:
+                            raise TimeoutError, 'Unable to get database ' \
+                                                'connection within %d seconds' \
+                                                % timeout
+                    else:
+                        self._available.wait()
+            self._active[tid] = [1, cnx]
+            return PooledConnection(self, cnx)
+        finally:
+            self._available.release()
+
+    def _return_cnx(self, cnx):
+        self._available.acquire()
+        try:
+            tid = threading._get_ident()
+            if tid in self._active:
+                num, cnx_ = self._active.get(tid)
+                assert cnx is cnx_
+                if num > 1:
+                    self._active[tid][0] = num - 1
+                else:
+                    del self._active[tid]
+                    if cnx not in self._dormant:
+                        cnx.rollback()
+                        if cnx.poolable:
+                            self._dormant.append(cnx)
+                        else:
+                            self._cursize -= 1
+                        self._available.notify()
+        finally:
+            self._available.release()
+
+    def shutdown(self):
+        self._available.acquire()
+        try:
+            for cnx in self._dormant:
+                cnx.cnx.close()
+        finally:
+            self._available.release()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/db/postgres_backend.py
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+import re
+
+from trac.core import *
+from trac.db.api import IDatabaseConnector
+from trac.db.util import ConnectionWrapper
+
+psycopg = None
+PgSQL = None
+PGSchemaError = None
+
+_like_escape_re = re.compile(r'([/_%])')
+
+
+class PostgreSQLConnector(Component):
+    """PostgreSQL database support."""
+
+    implements(IDatabaseConnector)
+
+    def get_supported_schemes(self):
+        return [('postgres', 1)]
+
+    def get_connection(self, path, user=None, password=None, host=None,
+                       port=None, params={}):
+        return PostgreSQLConnection(path, user, password, host, port, params)
+
+    def init_db(self, path, user=None, password=None, host=None, port=None,
+                params={}):
+        cnx = self.get_connection(path, user, password, host, port, params)
+        cursor = cnx.cursor()
+        if cnx.schema:
+            cursor.execute('CREATE SCHEMA %s' % cnx.schema)
+            cursor.execute('SET search_path TO %s, public', (cnx.schema,))
+        from trac.db_default import schema
+        for table in schema:
+            for stmt in self.to_sql(table):
+                cursor.execute(stmt)
+        cnx.commit()
+
+    def to_sql(self, table):
+        sql = ["CREATE TABLE %s (" % table.name]
+        coldefs = []
+        for column in table.columns:
+            ctype = column.type
+            if column.auto_increment:
+                ctype = "SERIAL"
+            if len(table.key) == 1 and column.name in table.key:
+                ctype += " PRIMARY KEY"
+            coldefs.append("    %s %s" % (column.name, ctype))
+        if len(table.key) > 1:
+            coldefs.append("    CONSTRAINT %s_pk PRIMARY KEY (%s)"
+                           % (table.name, ','.join(table.key)))
+        sql.append(',\n'.join(coldefs) + '\n)')
+        yield '\n'.join(sql)
+        for index in table.indices:
+            yield "CREATE INDEX %s_%s_idx ON %s (%s)" % (table.name, 
+                   '_'.join(index.columns), table.name, ','.join(index.columns))
+
+
+class PostgreSQLConnection(ConnectionWrapper):
+    """Connection wrapper for PostgreSQL."""
+
+    poolable = True
+
+    def __init__(self, path, user=None, password=None, host=None, port=None,
+                 params={}):
+        if path.startswith('/'):
+            path = path[1:]
+        # We support both psycopg and PgSQL but prefer psycopg
+        global psycopg
+        global PgSQL
+        global PGSchemaError
+        
+        if not psycopg and not PgSQL:
+            try:
+                import psycopg2 as psycopg
+                import psycopg2.extensions
+                from psycopg2 import ProgrammingError as PGSchemaError
+                psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
+            except ImportError:
+                from pyPgSQL import PgSQL
+                from pyPgSQL.libpq import OperationalError as PGSchemaError
+        if psycopg:
+            dsn = []
+            if path:
+                dsn.append('dbname=' + path)
+            if user:
+                dsn.append('user=' + user)
+            if password:
+                dsn.append('password=' + password)
+            if host:
+                dsn.append('host=' + host)
+            if port:
+                dsn.append('port=' + str(port))
+            cnx = psycopg.connect(' '.join(dsn))
+            cnx.set_client_encoding('UNICODE')
+        else:
+            cnx = PgSQL.connect('', user, password, host, path, port, 
+                                client_encoding='utf-8', unicode_results=True)
+        try:
+            self.schema = None
+            if 'schema' in params:
+                self.schema = params['schema']
+            cnx.cursor().execute('SET search_path TO %s, public', 
+                                (self.schema,))
+        except PGSchemaError:
+            cnx.rollback()
+        ConnectionWrapper.__init__(self, cnx)
+
+    def cast(self, column, type):
+        # Temporary hack needed for the union of selects in the search module
+        return 'CAST(%s AS %s)' % (column, type)
+
+    def like(self):
+        # Temporary hack needed for the case-insensitive string matching in the
+        # search module
+        return "ILIKE %s ESCAPE '/'"
+
+    def like_escape(self, text):
+        return _like_escape_re.sub(r'/\1', text)
+
+    def get_last_id(self, cursor, table, column='id'):
+        cursor.execute("SELECT CURRVAL('%s_%s_seq')" % (table, column))
+        return cursor.fetchone()[0]
+
+    def rollback(self):
+        self.cnx.rollback()
+        if self.schema:
+            try:
+                self.cnx.cursor().execute("SET search_path TO %s, public", 
+                                         (self.schema,))
+            except PGSchemaError:
+                self.cnx.rollback()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/db/schema.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+
+class Table(object):
+    """Declare a table in a database schema."""
+
+    def __init__(self, name, key=[]):
+        self.name = name
+        self.columns = []
+        self.indices = []
+        self.key = key
+        if isinstance(key, basestring):
+            self.key = [key]
+
+    def __getitem__(self, objs):
+        self.columns = [o for o in objs if isinstance(o, Column)]
+        self.indices = [o for o in objs if isinstance(o, Index)]
+        return self
+
+
+class Column(object):
+    """Declare a table column in a database schema."""
+
+    def __init__(self, name, type='text', size=None, unique=False,
+                 auto_increment=False):
+        self.name = name
+        self.type = type
+        self.size = size
+        self.auto_increment = auto_increment
+
+
+class Index(object):
+    """Declare an index for a database schema."""
+
+    def __init__(self, columns):
+        self.columns = columns
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/db/sqlite_backend.py
@@ -0,0 +1,197 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+import os
+import re
+import weakref
+
+from trac.core import *
+from trac.db.api import IDatabaseConnector
+from trac.db.util import ConnectionWrapper
+
+_like_escape_re = re.compile(r'([/_%])')
+
+try:
+    import pysqlite2.dbapi2 as sqlite
+    have_pysqlite = 2
+    _ver = sqlite.sqlite_version_info
+    sqlite_version = _ver[0] * 10000 + _ver[1] * 100 + int(_ver[2])
+
+    class PyFormatCursor(sqlite.Cursor):
+        def _rollback_on_error(self, function, *args, **kwargs):
+            try:
+                return function(self, *args, **kwargs)
+            except sqlite.OperationalError, e:
+                self.cnx.rollback()
+                raise
+        def execute(self, sql, args=None):
+            if args:
+                sql = sql % (('?',) * len(args))
+            return self._rollback_on_error(sqlite.Cursor.execute, sql,
+                                           args or [])
+        def executemany(self, sql, args=None):
+            if args:
+                sql = sql % (('?',) * len(args[0]))
+            return self._rollback_on_error(sqlite.Cursor.executemany, sql,
+                                           args or [])
+
+except ImportError:
+    try:
+        import sqlite
+        have_pysqlite = 1
+        _ver = sqlite._sqlite.sqlite_version_info()
+        sqlite_version = _ver[0] * 10000 + _ver[1] * 100 + _ver[2]
+
+        class SQLiteUnicodeCursor(sqlite.Cursor):
+            def _convert_row(self, row):
+                return tuple([(isinstance(v, str) and [v.decode('utf-8')] or [v])[0]
+                              for v in row])
+            def fetchone(self):
+                row = sqlite.Cursor.fetchone(self)
+                return row and self._convert_row(row) or None
+            def fetchmany(self, num):
+                rows = sqlite.Cursor.fetchmany(self, num)
+                return rows != None and [self._convert_row(row)
+                                         for row in rows] or []
+            def fetchall(self):
+                rows = sqlite.Cursor.fetchall(self)
+                return rows != None and [self._convert_row(row)
+                                         for row in rows] or []
+    except ImportError:
+        have_pysqlite = 0
+
+def _to_sql(table):
+    sql = ["CREATE TABLE %s (" % table.name]
+    coldefs = []
+    for column in table.columns:
+        ctype = column.type.lower()
+        if column.auto_increment:
+            ctype = "integer PRIMARY KEY"
+        elif len(table.key) == 1 and column.name in table.key:
+            ctype += " PRIMARY KEY"
+        elif ctype == "int":
+            ctype = "integer"
+        coldefs.append("    %s %s" % (column.name, ctype))
+    if len(table.key) > 1:
+        coldefs.append("    UNIQUE (%s)" % ','.join(table.key))
+    sql.append(',\n'.join(coldefs) + '\n);')
+    yield '\n'.join(sql)
+    for index in table.indices:
+        yield "CREATE INDEX %s_%s_idx ON %s (%s);" % (table.name,
+              '_'.join(index.columns), table.name, ','.join(index.columns))
+
+
+
+
+class SQLiteConnector(Component):
+    """SQLite database support."""
+    implements(IDatabaseConnector)
+
+    def get_supported_schemes(self):
+        return [('sqlite', 1)]
+
+    def get_connection(self, path, params={}):
+        return SQLiteConnection(path, params)
+
+    def init_db(cls, path, params={}):
+        if path != ':memory:':
+            # make the directory to hold the database
+            if os.path.exists(path):
+                raise TracError, 'Database already exists at %s' % path
+            os.makedirs(os.path.split(path)[0])
+        cnx = sqlite.connect(path, timeout=int(params.get('timeout', 10000)))
+        cursor = cnx.cursor()
+        from trac.db_default import schema
+        for table in schema:
+            for stmt in cls.to_sql(table):
+                cursor.execute(stmt)
+        cnx.commit()
+
+    def to_sql(cls, table):
+        return _to_sql(table)
+
+
+class SQLiteConnection(ConnectionWrapper):
+    """Connection wrapper for SQLite."""
+
+    __slots__ = ['_active_cursors']
+    poolable = False
+
+    def __init__(self, path, params={}):
+        assert have_pysqlite > 0
+        self.cnx = None
+        if path != ':memory:':
+            if not os.access(path, os.F_OK):
+                raise TracError, 'Database "%s" not found.' % path
+
+            dbdir = os.path.dirname(path)
+            if not os.access(path, os.R_OK + os.W_OK) or \
+                   not os.access(dbdir, os.R_OK + os.W_OK):
+                from getpass import getuser
+                raise TracError, 'The user %s requires read _and_ write ' \
+                                 'permission to the database file %s and the ' \
+                                 'directory it is located in.' \
+                                 % (getuser(), path)
+
+        if have_pysqlite == 2:
+            self._active_cursors = weakref.WeakKeyDictionary()
+            timeout = int(params.get('timeout', 10.0))
+            cnx = sqlite.connect(path, detect_types=sqlite.PARSE_DECLTYPES,
+                                 timeout=timeout)
+        else:
+            timeout = int(params.get('timeout', 10000))
+            cnx = sqlite.connect(path, timeout=timeout, encoding='utf-8')
+            
+        ConnectionWrapper.__init__(self, cnx)
+
+    if have_pysqlite == 2:
+        def cursor(self):
+            cursor = self.cnx.cursor(PyFormatCursor)
+            self._active_cursors[cursor] = True
+            cursor.cnx = self
+            return cursor
+
+        def rollback(self):
+            for cursor in self._active_cursors.keys():
+                cursor.close()
+            self.cnx.rollback()
+
+    else:
+        def cursor(self):
+            self.cnx._checkNotClosed("cursor")
+            return SQLiteUnicodeCursor(self.cnx, self.cnx.rowclass)
+
+    def cast(self, column, type):
+        return column
+
+    def like(self):
+        if sqlite_version >= 30100:
+            return "LIKE %s ESCAPE '/'"
+        else:
+            return 'LIKE %s'
+
+    def like_escape(self, text):
+        if sqlite_version >= 30100:
+            return _like_escape_re.sub(r'/\1', text)
+        else:
+            return text
+
+    if have_pysqlite == 2:
+        def get_last_id(self, cursor, table, column='id'):
+            return cursor.lastrowid
+    else:
+        def get_last_id(self, cursor, table, column='id'):
+            return self.cnx.db.sqlite_last_insert_rowid()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/db/tests/__init__.py
@@ -0,0 +1,13 @@
+import unittest
+
+from trac.db.tests import api
+
+def suite():
+
+    suite = unittest.TestSuite()
+    suite.addTest(api.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/db/tests/api.py
@@ -0,0 +1,57 @@
+from trac.db.api import _parse_db_str
+
+import unittest
+
+
+class ParseConnectionStringTestCase(unittest.TestCase):
+
+    def test_sqlite_relative(self):
+        # Default syntax for specifying DB path relative to the environment
+        # directory
+        self.assertEqual(('sqlite', {'path': 'db/trac.db'}),
+                         _parse_db_str('sqlite:db/trac.db'))
+
+    def test_sqlite_absolute(self):
+        # Standard syntax
+        self.assertEqual(('sqlite', {'path': '/var/db/trac.db'}),
+                         _parse_db_str('sqlite:///var/db/trac.db'))
+        # Legacy syntax
+        self.assertEqual(('sqlite', {'path': '/var/db/trac.db'}),
+                         _parse_db_str('sqlite:/var/db/trac.db'))
+
+    def test_sqlite_with_timeout_param(self):
+        # In-memory database
+        self.assertEqual(('sqlite', {'path': 'db/trac.db',
+                                     'params': {'timeout': '10000'}}),
+                         _parse_db_str('sqlite:db/trac.db?timeout=10000'))
+
+    def test_postgres_simple(self):
+        self.assertEqual(('postgres', {'host': 'localhost', 'path': '/trac'}),
+                         _parse_db_str('postgres://localhost/trac'))
+
+    def test_postgres_with_port(self):
+        self.assertEqual(('postgres', {'host': 'localhost', 'port': 9431,
+                                       'path': '/trac'}),
+                         _parse_db_str('postgres://localhost:9431/trac'))
+
+    def test_postgres_with_creds(self):
+        self.assertEqual(('postgres', {'user': 'john', 'password': 'letmein',
+                                       'host': 'localhost', 'port': 9431,
+                                       'path': '/trac'}),
+                         _parse_db_str('postgres://john:letmein@localhost:9431/trac'))
+
+    def test_mysql_simple(self):
+        self.assertEqual(('mysql', {'host': 'localhost', 'path': '/trac'}),
+                         _parse_db_str('mysql://localhost/trac'))
+
+    def test_mysql_with_creds(self):
+        self.assertEqual(('mysql', {'user': 'john', 'password': 'letmein',
+                                    'host': 'localhost', 'port': 3306,
+                                    'path': '/trac'}),
+                         _parse_db_str('mysql://john:letmein@localhost:3306/trac'))
+
+def suite():
+    return unittest.makeSuite(ParseConnectionStringTestCase,'test')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/db/util.py
@@ -0,0 +1,73 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2006 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2006 Matthew Good <trac@matt-good.net>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+def sql_escape_percent(sql):
+    import re
+    return re.sub("'((?:[^']|(?:''))*)'", lambda m: m.group(0).replace('%', '%%'), sql)
+
+
+class IterableCursor(object):
+    """Wrapper for DB-API cursor objects that makes the cursor iterable
+    and escapes all "%"s used inside literal strings with parameterized
+    queries.
+    
+    Iteration will generate the rows of a SELECT query one by one.
+    """
+    __slots__ = ['cursor']
+
+    def __init__(self, cursor):
+        self.cursor = cursor
+
+    def __getattr__(self, name):
+        return getattr(self.cursor, name)
+
+    def __iter__(self):
+        while True:
+            row = self.cursor.fetchone()
+            if not row:
+                return
+            yield row
+
+    def execute(self, sql, args=None):
+        if args:
+            return self.cursor.execute(sql_escape_percent(sql), args)
+        return self.cursor.execute(sql)
+
+    def executemany(self, sql, args=None):
+        if args:
+            return self.cursor.executemany(sql_escape_percent(sql), args)
+        return self.cursor.executemany(sql)
+
+
+class ConnectionWrapper(object):
+    """Generic wrapper around connection objects.
+    
+    This wrapper makes cursors produced by the connection iterable using
+    `IterableCursor`.
+    """
+    __slots__ = ['cnx']
+
+    def __init__(self, cnx):
+        self.cnx = cnx
+
+    def __getattr__(self, name):
+        if hasattr(self, 'cnx'):
+            return getattr(self.cnx, name)
+        return object.__getattr__(self, name)
+
+    def cursor(self):
+        return IterableCursor(self.cnx.cursor())
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/db_default.py
@@ -0,0 +1,406 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Daniel Lundin <daniel@edgewall.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Daniel Lundin <daniel@edgewall.com>
+
+from trac.db import Table, Column, Index
+
+# Database version identifier. Used for automatic upgrades.
+db_version = 19
+
+def __mkreports(reports):
+    """Utility function used to create report data in same syntax as the
+    default data. This extra step is done to simplify editing the default
+    reports."""
+    result = []
+    for report in reports:
+        result.append((None, report[0], report[2], report[1]))
+    return result
+
+
+##
+## Database schema
+##
+
+schema = [
+    # Common
+    Table('system', key='name')[
+        Column('name'),
+        Column('value')],
+    Table('permission', key=('username', 'action'))[
+        Column('username'),
+        Column('action')],
+    Table('auth_cookie', key=('cookie', 'ipnr', 'name'))[
+        Column('cookie'),
+        Column('name'),
+        Column('ipnr'),
+        Column('time', type='int')],
+    Table('session', key=('sid', 'authenticated'))[
+        Column('sid'),
+        Column('authenticated', type='int'),
+        Column('last_visit', type='int'),
+        Index(['last_visit']),
+        Index(['authenticated'])],
+    Table('session_attribute', key=('sid', 'authenticated', 'name'))[
+        Column('sid'),
+        Column('authenticated', type='int'),
+        Column('name'),
+        Column('value')],
+
+    # Attachments
+    Table('attachment', key=('type', 'id', 'filename'))[
+        Column('type'),
+        Column('id'),
+        Column('filename'),
+        Column('size', type='int'),
+        Column('time', type='int'),
+        Column('description'),
+        Column('author'),
+        Column('ipnr')],
+
+    # Wiki system
+    Table('wiki', key=('name', 'version'))[
+        Column('name'),
+        Column('version', type='int'),
+        Column('time', type='int'),
+        Column('author'),
+        Column('ipnr'),
+        Column('text'),
+        Column('comment'),
+        Column('readonly', type='int'),
+        Index(['time'])],
+
+    # Version control cache
+    Table('revision', key='rev')[
+        Column('rev'),
+        Column('time', type='int'),
+        Column('author'),
+        Column('message'),
+        Index(['time'])],
+    Table('node_change', key=('rev', 'path', 'change_type'))[
+        Column('rev'),
+        Column('path'),
+        Column('node_type', size=1),
+        Column('change_type', size=1),
+        Column('base_path'),
+        Column('base_rev'),
+        Index(['rev'])],
+
+    # Ticket system
+    Table('ticket', key='id')[
+        Column('id', auto_increment=True),
+        Column('type'),
+        Column('time', type='int'),
+        Column('changetime', type='int'),
+        Column('component'),
+        Column('severity'),
+        Column('priority'),
+        Column('owner'),
+        Column('reporter'),
+        Column('cc'),
+        Column('version'),
+        Column('milestone'),
+        Column('status'),
+        Column('resolution'),
+        Column('summary'),
+        Column('description'),
+        Column('keywords'),
+        Index(['time']),
+        Index(['status'])],    
+    Table('ticket_change', key=('ticket', 'time', 'field'))[
+        Column('ticket', type='int'),
+        Column('time', type='int'),
+        Column('author'),
+        Column('field'),
+        Column('oldvalue'),
+        Column('newvalue'),
+        Index(['ticket']),
+        Index(['time'])],
+    Table('ticket_custom', key=('ticket', 'name'))[
+        Column('ticket', type='int'),
+        Column('name'),
+        Column('value')],
+    Table('enum', key=('type', 'name'))[
+        Column('type'),
+        Column('name'),
+        Column('value')],
+    Table('component', key='name')[
+        Column('name'),
+        Column('owner'),
+        Column('description')],
+    Table('milestone', key='name')[
+        Column('name'),
+        Column('due', type='int'),
+        Column('completed', type='int'),
+        Column('description')],
+    Table('version', key='name')[
+        Column('name'),
+        Column('time', type='int'),
+        Column('description')],
+
+    # Report system
+    Table('report', key='id')[
+        Column('id', auto_increment=True),
+        Column('author'),
+        Column('title'),
+        Column('query'),
+        Column('description')],
+]
+
+
+##
+## Default Reports
+##
+
+reports = (
+('Active Tickets',
+"""
+ * List all active tickets by priority.
+ * Color each row based on priority.
+ * If a ticket has been accepted, a '*' is appended after the owner's name
+""",
+"""
+SELECT p.value AS __color__,
+   id AS ticket, summary, component, version, milestone, t.type AS type, 
+   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner,
+   time AS created,
+   changetime AS _changetime, description AS _description,
+   reporter AS _reporter
+  FROM ticket t, enum p
+  WHERE status IN ('new', 'assigned', 'reopened') 
+AND p.name = t.priority AND p.type = 'priority'
+  ORDER BY p.value, milestone, t.type, time
+"""),
+#----------------------------------------------------------------------------
+ ('Active Tickets by Version',
+"""
+This report shows how to color results by priority,
+while grouping results by version.
+
+Last modification time, description and reporter are included as hidden fields
+for useful RSS export.
+""",
+"""
+SELECT p.value AS __color__,
+   version AS __group__,
+   id AS ticket, summary, component, version, t.type AS type, 
+   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner,
+   time AS created,
+   changetime AS _changetime, description AS _description,
+   reporter AS _reporter
+  FROM ticket t, enum p
+  WHERE status IN ('new', 'assigned', 'reopened') 
+AND p.name = t.priority AND p.type = 'priority'
+  ORDER BY (version IS NULL),version, p.value, t.type, time
+"""),
+#----------------------------------------------------------------------------
+('All Tickets by Milestone',
+"""
+This report shows how to color results by priority,
+while grouping results by milestone.
+
+Last modification time, description and reporter are included as hidden fields
+for useful RSS export.
+""",
+"""
+SELECT p.value AS __color__,
+   milestone||' Release' AS __group__,
+   id AS ticket, summary, component, version, t.type AS type, 
+   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner,
+   time AS created,
+   changetime AS _changetime, description AS _description,
+   reporter AS _reporter
+  FROM ticket t, enum p
+  WHERE status IN ('new', 'assigned', 'reopened') 
+AND p.name = t.priority AND p.type = 'priority'
+  ORDER BY (milestone IS NULL),milestone, p.value, t.type, time
+"""),
+#----------------------------------------------------------------------------
+('Assigned, Active Tickets by Owner',
+"""
+List assigned tickets, group by ticket owner, sorted by priority.
+""",
+"""
+
+SELECT p.value AS __color__,
+   owner AS __group__,
+   id AS ticket, summary, component, milestone, t.type AS type, time AS created,
+   changetime AS _changetime, description AS _description,
+   reporter AS _reporter
+  FROM ticket t,enum p
+  WHERE status = 'assigned'
+AND p.name=t.priority AND p.type='priority'
+  ORDER BY owner, p.value, t.type, time
+"""),
+#----------------------------------------------------------------------------
+('Assigned, Active Tickets by Owner (Full Description)',
+"""
+List tickets assigned, group by ticket owner.
+This report demonstrates the use of full-row display.
+""",
+"""
+SELECT p.value AS __color__,
+   owner AS __group__,
+   id AS ticket, summary, component, milestone, t.type AS type, time AS created,
+   description AS _description_,
+   changetime AS _changetime, reporter AS _reporter
+  FROM ticket t, enum p
+  WHERE status = 'assigned'
+AND p.name = t.priority AND p.type = 'priority'
+  ORDER BY owner, p.value, t.type, time
+"""),
+#----------------------------------------------------------------------------
+('All Tickets By Milestone  (Including closed)',
+"""
+A more complex example to show how to make advanced reports.
+""",
+"""
+SELECT p.value AS __color__,
+   t.milestone AS __group__,
+   (CASE status 
+      WHEN 'closed' THEN 'color: #777; background: #ddd; border-color: #ccc;'
+      ELSE 
+        (CASE owner WHEN '$USER' THEN 'font-weight: bold' END)
+    END) AS __style__,
+   id AS ticket, summary, component, status, 
+   resolution,version, t.type AS type, priority, owner,
+   changetime AS modified,
+   time AS _time,reporter AS _reporter
+  FROM ticket t,enum p
+  WHERE p.name=t.priority AND p.type='priority'
+  ORDER BY (milestone IS NULL), milestone DESC, (status = 'closed'), 
+        (CASE status WHEN 'closed' THEN modified ELSE (-1)*p.value END) DESC
+"""),
+#----------------------------------------------------------------------------
+('My Tickets',
+"""
+This report demonstrates the use of the automatically set 
+USER dynamic variable, replaced with the username of the
+logged in user when executed.
+""",
+"""
+SELECT p.value AS __color__,
+   (CASE status WHEN 'assigned' THEN 'Assigned' ELSE 'Owned' END) AS __group__,
+   id AS ticket, summary, component, version, milestone,
+   t.type AS type, priority, time AS created,
+   changetime AS _changetime, description AS _description,
+   reporter AS _reporter
+  FROM ticket t, enum p
+  WHERE t.status IN ('new', 'assigned', 'reopened') 
+AND p.name = t.priority AND p.type = 'priority' AND owner = '$USER'
+  ORDER BY (status = 'assigned') DESC, p.value, milestone, t.type, time
+"""),
+#----------------------------------------------------------------------------
+('Active Tickets, Mine first',
+"""
+ * List all active tickets by priority.
+ * Show all tickets owned by the logged in user in a group first.
+""",
+"""
+SELECT p.value AS __color__,
+   (CASE owner 
+     WHEN '$USER' THEN 'My Tickets' 
+     ELSE 'Active Tickets' 
+    END) AS __group__,
+   id AS ticket, summary, component, version, milestone, t.type AS type, 
+   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner,
+   time AS created,
+   changetime AS _changetime, description AS _description,
+   reporter AS _reporter
+  FROM ticket t, enum p
+  WHERE status IN ('new', 'assigned', 'reopened') 
+AND p.name = t.priority AND p.type = 'priority'
+  ORDER BY (owner = '$USER') DESC, p.value, milestone, t.type, time
+"""))
+
+
+##
+## Default database values
+##
+
+# (table, (column1, column2), ((row1col1, row1col2), (row2col1, row2col2)))
+data = (('component',
+             ('name', 'owner'),
+               (('component1', 'somebody'),
+                ('component2', 'somebody'))),
+           ('milestone',
+             ('name', 'due', 'completed'),
+               (('milestone1', 0, 0),
+                ('milestone2', 0, 0),
+                ('milestone3', 0, 0),
+                ('milestone4', 0, 0))),
+           ('version',
+             ('name', 'time'),
+               (('1.0', 0),
+                ('2.0', 0))),
+           ('enum',
+             ('type', 'name', 'value'),
+               (('status', 'new', 1),
+                ('status', 'assigned', 2),
+                ('status', 'reopened', 3),
+                ('status', 'closed', 4),
+                ('resolution', 'fixed', 1),
+                ('resolution', 'invalid', 2),
+                ('resolution', 'wontfix', 3),
+                ('resolution', 'duplicate', 4),
+                ('resolution', 'worksforme', 5),
+                ('priority', 'blocker', 1),
+                ('priority', 'critical', 2),
+                ('priority', 'major', 3),
+                ('priority', 'minor', 4),
+                ('priority', 'trivial', 5),
+                ('ticket_type', 'defect', 1),
+                ('ticket_type', 'enhancement', 2),
+                ('ticket_type', 'task', 3))),
+           ('permission',
+             ('username', 'action'),
+               (('anonymous', 'LOG_VIEW'),
+                ('anonymous', 'FILE_VIEW'),
+                ('anonymous', 'WIKI_VIEW'),
+                ('anonymous', 'WIKI_CREATE'),
+                ('anonymous', 'WIKI_MODIFY'),
+                ('anonymous', 'SEARCH_VIEW'),
+                ('anonymous', 'REPORT_VIEW'),
+                ('anonymous', 'REPORT_SQL_VIEW'),
+                ('anonymous', 'TICKET_VIEW'),
+                ('anonymous', 'TICKET_CREATE'),
+                ('anonymous', 'TICKET_MODIFY'),
+                ('anonymous', 'BROWSER_VIEW'),
+                ('anonymous', 'TIMELINE_VIEW'),
+                ('anonymous', 'CHANGESET_VIEW'),
+                ('anonymous', 'ROADMAP_VIEW'),
+                ('anonymous', 'MILESTONE_VIEW'))),
+           ('system',
+             ('name', 'value'),
+               (('database_version', str(db_version)),)),
+           ('report',
+             ('author', 'title', 'query', 'description'),
+               __mkreports(reports)))
+
+
+default_components = ('trac.About', 'trac.attachment',
+                      'trac.db.mysql_backend', 'trac.db.postgres_backend',
+                      'trac.db.sqlite_backend',
+                      'trac.mimeview.enscript', 'trac.mimeview.patch',
+                      'trac.mimeview.php', 'trac.mimeview.rst',
+                      'trac.mimeview.silvercity', 'trac.mimeview.txtl',
+                      'trac.scripts.admin',
+                      'trac.Search', 'trac.Settings',
+                      'trac.ticket.query', 'trac.ticket.report',
+                      'trac.ticket.roadmap', 'trac.ticket.web_ui',
+                      'trac.Timeline',
+                      'trac.versioncontrol.web_ui',
+                      'trac.versioncontrol.svn_fs',
+                      'trac.wiki.macros', 'trac.wiki.web_ui',
+                      'trac.web.auth')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/env.py
@@ -0,0 +1,435 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+
+import os
+
+from trac import db_default, util
+from trac.config import *
+from trac.core import Component, ComponentManager, implements, Interface, \
+                      ExtensionPoint, TracError
+from trac.db import DatabaseManager
+from trac.versioncontrol import RepositoryManager
+
+__all__ = ['Environment', 'IEnvironmentSetupParticipant', 'open_environment']
+
+
+class IEnvironmentSetupParticipant(Interface):
+    """Extension point interface for components that need to participate in the
+    creation and upgrading of Trac environments, for example to create
+    additional database tables."""
+
+    def environment_created():
+        """Called when a new Trac environment is created."""
+
+    def environment_needs_upgrade(db):
+        """Called when Trac checks whether the environment needs to be upgraded.
+        
+        Should return `True` if this participant needs an upgrade to be
+        performed, `False` otherwise.
+        """
+
+    def upgrade_environment(db):
+        """Actually perform an environment upgrade.
+        
+        Implementations of this method should not commit any database
+        transactions. This is done implicitly after all participants have
+        performed the upgrades they need without an error being raised.
+        """
+
+
+class Environment(Component, ComponentManager):
+    """Trac stores project information in a Trac environment.
+
+    A Trac environment consists of a directory structure containing among other
+    things:
+     * a configuration file.
+     * an SQLite database (stores tickets, wiki pages...)
+     * Project specific templates and wiki macros.
+     * wiki and ticket attachments.
+    """   
+    setup_participants = ExtensionPoint(IEnvironmentSetupParticipant)
+
+    base_url = Option('trac', 'base_url', '',
+        """Base URL of the Trac deployment.
+        
+        In most configurations, Trac will automatically reconstruct the URL
+        that is used to access it automatically. However, in more complex
+        setups, usually involving running Trac behind a HTTP proxy, you may
+        need to use this option to force Trac to use the correct URL.""")
+
+    project_name = Option('project', 'name', 'My Project',
+        """Name of the project.""")
+
+    project_description = Option('project', 'descr', 'My example project',
+        """Short description of the project.""")
+
+    project_url = Option('project', 'url', 'http://example.org/',
+        """URL of the main project web site.""")
+
+    project_footer = Option('project', 'footer',
+                            'Visit the Trac open source project at<br />'
+                            '<a href="http://trac.edgewall.com/">'
+                            'http://trac.edgewall.com/</a>',
+        """Page footer text (right-aligned).""")
+
+    project_icon = Option('project', 'icon', 'common/trac.ico',
+        """URL of the icon of the project.""")
+
+    log_type = Option('logging', 'log_type', 'none',
+        """Logging facility to use.
+        
+        Should be one of (`none`, `file`, `stderr`, `syslog`, `winlog`).""")
+
+    log_file = Option('logging', 'log_file', 'trac.log',
+        """If `log_type` is `file`, this should be a path to the log-file.""")
+
+    log_level = Option('logging', 'log_level', 'DEBUG',
+        """Level of verbosity in log.
+        
+        Should be one of (`CRITICAL`, `ERROR`, `WARN`, `INFO`, `DEBUG`).""")
+
+    def __init__(self, path, create=False, options=[]):
+        """Initialize the Trac environment.
+        
+        @param path:   the absolute path to the Trac environment
+        @param create: if `True`, the environment is created and populated with
+                       default data; otherwise, the environment is expected to
+                       already exist.
+        @param options: A list of `(section, name, value)` tuples that define
+                        configuration options
+        """
+        ComponentManager.__init__(self)
+
+        self.path = path
+        self.setup_config(load_defaults=create)
+        self.setup_log()
+
+        from trac.loader import load_components
+        load_components(self)
+
+        if create:
+            self.create(options)
+        else:
+            self.verify()
+
+        if create:
+            for setup_participant in self.setup_participants:
+                setup_participant.environment_created()
+
+    def component_activated(self, component):
+        """Initialize additional member variables for components.
+        
+        Every component activated through the `Environment` object gets three
+        member variables: `env` (the environment object), `config` (the
+        environment configuration) and `log` (a logger object)."""
+        component.env = self
+        component.config = self.config
+        component.log = self.log
+
+    def is_component_enabled(self, cls):
+        """Implemented to only allow activation of components that are not
+        disabled in the configuration.
+        
+        This is called by the `ComponentManager` base class when a component is
+        about to be activated. If this method returns false, the component does
+        not get activated."""
+        if not isinstance(cls, basestring):
+            component_name = (cls.__module__ + '.' + cls.__name__).lower()
+        else:
+            component_name = cls.lower()
+
+        rules = [(name.lower(), value.lower() in ('enabled', 'on'))
+                 for name, value in self.config.options('components')]
+        rules.sort(lambda a, b: -cmp(len(a[0]), len(b[0])))
+
+        for pattern, enabled in rules:
+            if component_name == pattern or pattern.endswith('*') \
+                    and component_name.startswith(pattern[:-1]):
+                return enabled
+
+        # versioncontrol components are enabled if the repository is configured
+        # FIXME: this shouldn't be hardcoded like this
+        if component_name.startswith('trac.versioncontrol.'):
+            return self.config.get('trac', 'repository_dir') != ''
+
+        # By default, all components in the trac package are enabled
+        return component_name.startswith('trac.')
+
+    def verify(self):
+        """Verify that the provided path points to a valid Trac environment
+        directory."""
+        fd = open(os.path.join(self.path, 'VERSION'), 'r')
+        try:
+            assert fd.read(26) == 'Trac Environment Version 1'
+        finally:
+            fd.close()
+
+    def get_db_cnx(self):
+        """Return a database connection from the connection pool."""
+        return DatabaseManager(self).get_connection()
+
+    def shutdown(self):
+        """Close the environment."""
+        DatabaseManager(self).shutdown()
+
+    def get_repository(self, authname=None):
+        """Return the version control repository configured for this
+        environment.
+        
+        @param authname: user name for authorization
+        """
+        return RepositoryManager(self).get_repository(authname)
+
+    def create(self, options=[]):
+        """Create the basic directory structure of the environment, initialize
+        the database and populate the configuration file with default values."""
+        def _create_file(fname, data=None):
+            fd = open(fname, 'w')
+            if data: fd.write(data)
+            fd.close()
+
+        # Create the directory structure
+        if not os.path.exists(self.path):
+            os.mkdir(self.path)
+        os.mkdir(self.get_log_dir())
+        os.mkdir(self.get_htdocs_dir())
+        os.mkdir(os.path.join(self.path, 'plugins'))
+        os.mkdir(os.path.join(self.path, 'wiki-macros'))
+
+        # Create a few files
+        _create_file(os.path.join(self.path, 'VERSION'),
+                     'Trac Environment Version 1\n')
+        _create_file(os.path.join(self.path, 'README'),
+                     'This directory contains a Trac environment.\n'
+                     'Visit http://trac.edgewall.com/ for more information.\n')
+
+        # Setup the default configuration
+        os.mkdir(os.path.join(self.path, 'conf'))
+        _create_file(os.path.join(self.path, 'conf', 'trac.ini'))
+        self.setup_config(load_defaults=True)
+        for section, name, value in options:
+            self.config.set(section, name, value)
+        self.config.save()
+
+        # Create the database
+        DatabaseManager(self).init_db()
+
+    def get_version(self, db=None):
+        """Return the current version of the database."""
+        if not db:
+            db = self.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT value FROM system WHERE name='database_version'")
+        row = cursor.fetchone()
+        return row and int(row[0])
+
+    def setup_config(self, load_defaults=False):
+        """Load the configuration file."""
+        self.config = Configuration(os.path.join(self.path, 'conf', 'trac.ini'))
+        if load_defaults:
+            for section, default_options in self.config.defaults().iteritems():
+                for name, value in default_options.iteritems():
+                    self.config.set(section, name, value)
+
+    def get_templates_dir(self):
+        """Return absolute path to the templates directory."""
+        return os.path.join(self.path, 'templates')
+
+    def get_htdocs_dir(self):
+        """Return absolute path to the htdocs directory."""
+        return os.path.join(self.path, 'htdocs')
+
+    def get_log_dir(self):
+        """Return absolute path to the log directory."""
+        return os.path.join(self.path, 'log')
+
+    def setup_log(self):
+        """Initialize the logging sub-system."""
+        from trac.log import logger_factory
+        logtype = self.log_type
+        logfile = self.log_file
+        if logtype == 'file' and not os.path.isabs(logfile):
+            logfile = os.path.join(self.get_log_dir(), logfile)
+        self.log = logger_factory(logtype, logfile, self.log_level, self.path)
+
+    def get_known_users(self, cnx=None):
+        """Generator that yields information about all known users, i.e. users
+        that have logged in to this Trac environment and possibly set their name
+        and email.
+
+        This function generates one tuple for every user, of the form
+        (username, name, email) ordered alpha-numerically by username.
+
+        @param cnx: the database connection; if ommitted, a new connection is
+                    retrieved
+        """
+        if not cnx:
+            cnx = self.get_db_cnx()
+        cursor = cnx.cursor()
+        cursor.execute("SELECT DISTINCT s.sid, n.value, e.value "
+                       "FROM session AS s "
+                       " LEFT JOIN session_attribute AS n ON (n.sid=s.sid "
+                       "  and n.authenticated=1 AND n.name = 'name') "
+                       " LEFT JOIN session_attribute AS e ON (e.sid=s.sid "
+                       "  AND e.authenticated=1 AND e.name = 'email') "
+                       "WHERE s.authenticated=1 ORDER BY s.sid")
+        for username,name,email in cursor:
+            yield username, name, email
+
+    def backup(self, dest=None):
+        """Simple SQLite-specific backup of the database.
+
+        @param dest: Destination file; if not specified, the backup is stored in
+                     a file called db_name.trac_version.bak
+        """
+        import shutil
+
+        db_str = self.config.get('trac', 'database')
+        if not db_str.startswith('sqlite:'):
+            raise EnvironmentError, 'Can only backup sqlite databases'
+        db_name = os.path.join(self.path, db_str[7:])
+        if not dest:
+            dest = '%s.%i.bak' % (db_name, self.get_version())
+        shutil.copy (db_name, dest)
+
+    def needs_upgrade(self):
+        """Return whether the environment needs to be upgraded."""
+        db = self.get_db_cnx()
+        for participant in self.setup_participants:
+            if participant.environment_needs_upgrade(db):
+                self.log.warning('Component %s requires environment upgrade',
+                                 participant)
+                return True
+        return False
+
+    def upgrade(self, backup=False, backup_dest=None):
+        """Upgrade database.
+        
+        Each db version should have its own upgrade module, names
+        upgrades/dbN.py, where 'N' is the version number (int).
+
+        @param backup: whether or not to backup before upgrading
+        @param backup_dest: name of the backup file
+        @return: whether the upgrade was performed
+        """
+        db = self.get_db_cnx()
+
+        upgraders = []
+        for participant in self.setup_participants:
+            if participant.environment_needs_upgrade(db):
+                upgraders.append(participant)
+        if not upgraders:
+            return False
+
+        if backup:
+            self.backup(backup_dest)
+        for participant in upgraders:
+            participant.upgrade_environment(db)
+        db.commit()
+
+        # Database schema may have changed, so close all connections
+        self.shutdown()
+
+        return True
+
+
+class EnvironmentSetup(Component):
+    implements(IEnvironmentSetupParticipant)
+
+    # IEnvironmentSetupParticipant methods
+
+    def environment_created(self):
+        """Insert default data into the database."""
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        for table, cols, vals in db_default.data:
+            cursor.executemany("INSERT INTO %s (%s) VALUES (%s)" % (table,
+                               ','.join(cols), ','.join(['%s' for c in cols])),
+                               vals)
+        db.commit()
+        self._update_sample_config()
+
+    def environment_needs_upgrade(self, db):
+        dbver = self.env.get_version(db)
+        if dbver == db_default.db_version:
+            return False
+        elif dbver > db_default.db_version:
+            raise TracError, 'Database newer than Trac version'
+        return True
+
+    def upgrade_environment(self, db):
+        cursor = db.cursor()
+        dbver = self.env.get_version()
+        for i in range(dbver + 1, db_default.db_version + 1):
+            name  = 'db%i' % i
+            try:
+                upgrades = __import__('upgrades', globals(), locals(), [name])
+                script = getattr(upgrades, name)
+            except AttributeError:
+                err = 'No upgrade module for version %i (%s.py)' % (i, name)
+                raise TracError, err
+            script.do_upgrade(self.env, i, cursor)
+        cursor.execute("UPDATE system SET value=%s WHERE "
+                       "name='database_version'", (db_default.db_version,))
+        self.log.info('Upgraded database version from %d to %d',
+                      dbver, db_default.db_version)
+        self._update_sample_config()
+
+    # Internal methods
+
+    def _update_sample_config(self):
+        from ConfigParser import ConfigParser
+        config = ConfigParser()
+        for section, options in self.config.defaults().items():
+            config.add_section(section)
+            for name, value in options.items():
+                config.set(section, name, value)
+        filename = os.path.join(self.env.path, 'conf', 'trac.ini.sample')
+        try:
+            fileobj = file(filename, 'w')
+            try:
+                config.write(fileobj)
+                fileobj.close()
+            finally:
+                fileobj.close()
+            self.log.info('Wrote sample configuration file with the new '
+                          'settings and their default values: %s',
+                          filename)
+        except IOError, e:
+            self.log.warn('Couldn\'t write sample configuration file (%s)', e,
+                          exc_info=True)
+
+
+def open_environment(env_path=None):
+    """Open an existing environment object, and verify that the database is up
+    to date.
+
+    @param: env_path absolute path to the environment directory; if ommitted,
+            the value of the `TRAC_ENV` environment variable is used
+    @return: the `Environment` object
+    """
+    if not env_path:
+        env_path = os.getenv('TRAC_ENV')
+    if not env_path:
+        raise TracError, 'Missing environment variable "TRAC_ENV". Trac ' \
+                         'requires this variable to point to a valid Trac ' \
+                         'environment.'
+
+    env = Environment(env_path)
+    if env.needs_upgrade():
+        raise TracError, 'The Trac Environment needs to be upgraded. Run ' \
+                         'trac-admin %s upgrade"' % env_path
+    return env
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/loader.py
@@ -0,0 +1,140 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2006 Edgewall Software
+# Copyright (C) 2005-2006 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+from glob import glob
+import imp
+import os
+import sys
+from trac.config import default_dir
+try:
+    set
+except NameError:
+    from sets import Set as set
+
+try:
+    import pkg_resources
+except ImportError:
+    pkg_resources = None
+
+__all__ = ['load_components']
+
+def load_components(env):
+    loaded_components = []
+    plugins_dirs = [os.path.normcase(os.path.realpath(os.path.join(env.path,
+                                                                  'plugins'))),
+                    default_dir('plugins')]
+
+    # First look for Python source files in the plugins directories, which
+    # simply get imported, thereby registering them with the component manager
+    # if they define any components.
+    for plugins_dir in plugins_dirs:
+        auto_enable = plugins_dir != default_dir('plugins')
+        plugin_files = glob(os.path.join(plugins_dir, '*.py'))
+        for plugin_file in plugin_files:
+            try:
+                plugin_name = os.path.basename(plugin_file[:-3])
+                if plugin_name not in loaded_components:
+                    env.log.debug('Loading file plugin %s from %s' % (plugin_name,
+                                                                 plugin_file))
+                    module = imp.load_source(plugin_name, plugin_file)
+                    loaded_components.append(plugin_name)
+                    if auto_enable and plugin_name + '.*' \
+                            not in env.config['components']:
+                        env.config['components'].set(plugin_name + '.*', 'enabled')
+            except Exception, e:
+                env.log.error('Failed to load plugin from %s', plugin_file,
+                              exc_info=True)
+
+    # If setuptools is installed try to load any eggs from the plugins
+    # directory, and also plugins available on sys.path
+    if pkg_resources is not None:
+        ws = pkg_resources.working_set
+        for plugins_dir in plugins_dirs:
+            ws.add_entry(plugins_dir)
+        pkg_env = pkg_resources.Environment(plugins_dirs + sys.path)
+
+        memo = set()
+        def flatten(dists):
+             for dist in dists:
+                 if dist in memo:
+                     continue
+                 memo.add(dist)
+                 try:
+                     predecessors = ws.resolve([dist.as_requirement()])
+                     for predecessor in flatten(predecessors):
+                         yield predecessor
+                     yield dist
+                 except pkg_resources.DistributionNotFound, e:
+                     env.log.error('Skipping "%s" ("%s" not found)', dist, e)
+                 except pkg_resources.VersionConflict, e:
+                     env.log.error('Skipping "%s" (version conflict: "%s")',
+                                   dist, e)
+
+        for egg in flatten([pkg_env[name][0] for name in pkg_env]):
+            modules = []
+
+            for name in egg.get_entry_map('trac.plugins'):
+                # Load plugins declared via the `trac.plugins` entry point.
+                # This is the only supported option going forward, the
+                # others will be dropped at some point in the future.
+                env.log.debug('Loading egg plugin %s from %s', name,
+                              egg.location)
+                egg.activate()
+                try:
+                    entry_point = egg.get_entry_info('trac.plugins', name)
+                    if entry_point.module_name not in loaded_components:
+                        try:
+                            entry_point.load()
+                        except pkg_resources.DistributionNotFound, e:
+                            env.log.warning('Cannot load plugin %s because it '
+                                            'requires "%s"', name, e)
+                        modules.append(entry_point.module_name)
+                        loaded_components.append(entry_point.module_name)
+                except ImportError, e:
+                    env.log.error('Failed to load plugin %s from %s', name,
+                                  egg.location, exc_info=True)
+
+            else:
+                # Support for pre-entry-point plugins
+                if egg.has_metadata('trac_plugin.txt'):
+                    env.log.debug('Loading plugin %s from %s', name,
+                                  egg.location)
+                    egg.activate()
+                    for modname in egg.get_metadata_lines('trac_plugin.txt'):
+                        module = None
+                        if modname not in loaded_components:
+                            try:
+                                module = __import__(modname)
+                                loaded_components.append(modname)
+                                modules.append(modname)
+                            except ImportError, e:
+                                env.log.error('Component module %s not found',
+                                              modname, exc_info=True)
+
+            if modules:
+                # Automatically enable any components provided by plugins
+                # loaded from the environment plugins directory.
+                if os.path.dirname(egg.location) == plugins_dirs[0]:
+                    for module in modules:
+                        if module + '.*' not in env.config['components']:
+                            env.config['components'].set(module + '.*',
+                                                         'enabled')
+
+    # Load default components
+    from trac.db_default import default_components
+    for module in default_components:
+        if not module in loaded_components:
+            __import__(module)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/log.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Daniel Lundin <daniel@edgewall.com>
+# Copyright (C) 2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Daniel Lundin <daniel@edgewall.com>
+
+import logging
+import logging.handlers
+import sys
+
+def logger_factory(logtype='syslog', logfile=None, level='WARNING',
+                   logid='Trac'):
+    logger = logging.getLogger(logid)
+    logtype = logtype.lower()
+    if logtype == 'file':
+        hdlr = logging.FileHandler(logfile)
+    elif logtype in ['winlog', 'eventlog', 'nteventlog']:
+        # Requires win32 extensions
+        hdlr = logging.handlers.NTEventLogHandler(logid,
+                                                  logtype='Application')
+    elif logtype in ['syslog', 'unix']:
+        hdlr = logging.handlers.SysLogHandler('/dev/log')
+    elif logtype in ['stderr']:
+        hdlr = logging.StreamHandler(sys.stderr)
+    else:
+        hdlr = logging.handlers.BufferingHandler(0)
+        # Note: this _really_ throws away log events, as a `MemoryHandler`
+        # would keep _all_ records in case there's no target handler (a bug?)
+
+    format = 'Trac[%(module)s] %(levelname)s: %(message)s'
+    if logtype in ['file', 'stderr']:
+        format = '%(asctime)s ' + format 
+    datefmt = ''
+    if logtype == 'stderr':
+        datefmt = '%X'        
+    level = level.upper()
+    if level in ['DEBUG', 'ALL']:
+        logger.setLevel(logging.DEBUG)
+    elif level == 'INFO':
+        logger.setLevel(logging.INFO)
+    elif level == 'ERROR':
+        logger.setLevel(logging.ERROR)
+    elif level == 'CRITICAL':
+        logger.setLevel(logging.CRITICAL)
+    else:
+        logger.setLevel(logging.WARNING)
+    formatter = logging.Formatter(format,datefmt)
+    hdlr.setFormatter(formatter)
+    logger.addHandler(hdlr) 
+
+    return logger
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/mimeview/__init__.py
@@ -0,0 +1,1 @@
+from trac.mimeview.api import *
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/mimeview/api.py
@@ -0,0 +1,668 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2006 Edgewall Software
+# Copyright (C) 2004 Daniel Lundin <daniel@edgewall.com>
+# Copyright (C) 2005-2006 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Daniel Lundin <daniel@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+#         Christian Boos <cboos@neuf.fr>
+
+"""
+The `trac.mimeview` module centralize the intelligence related to
+file metadata, principally concerning the `type` (MIME type) of the content
+and, if relevant, concerning the text encoding (charset) used by the content.
+
+There are primarily two approaches for getting the MIME type of a given file:
+ * taking advantage of existing conventions for the file name
+ * examining the file content and applying various heuristics
+
+The module also knows how to convert the file content from one type
+to another type.
+
+In some cases, only the `url` pointing to the file's content is actually
+needed, that's why we avoid to read the file's content when it's not needed.
+
+The actual `content` to be converted might be a `unicode` object,
+but it can also be the raw byte string (`str`) object, or simply
+an object that can be `read()`.
+"""
+
+import re
+from StringIO import StringIO
+
+from trac.config import IntOption, ListOption, Option
+from trac.core import *
+from trac.util import sorted
+from trac.util.text import to_utf8, to_unicode
+from trac.util.markup import escape, Markup, Fragment, html
+
+
+__all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 'Mimeview',
+           'content_to_unicode']
+
+
+# Some common MIME types and their associated keywords and/or file extensions
+
+KNOWN_MIME_TYPES = {
+    'application/pdf':        ['pdf'],
+    'application/postscript': ['ps'],
+    'application/rtf':        ['rtf'],
+    'application/x-sh':       ['sh'],
+    'application/x-csh':      ['csh'],
+    'application/x-troff':    ['nroff', 'roff', 'troff'],
+
+    'image/x-icon':           ['ico'],
+    'image/svg+xml':          ['svg'],
+    
+    'model/vrml':             ['vrml', 'wrl'],
+    
+    'text/css':               ['css'],
+    'text/html':              ['html'],
+    'text/plain':             ['txt', 'TXT', 'text', 'README', 'INSTALL',
+                               'AUTHORS', 'COPYING', 'ChangeLog', 'RELEASE'],
+    'text/xml':               ['xml'],
+    'text/xsl':               ['xsl'],
+    'text/x-csrc':            ['c', 'xs'],
+    'text/x-chdr':            ['h'],
+    'text/x-c++src':          ['cc', 'CC', 'cpp', 'C'],
+    'text/x-c++hdr':          ['hh', 'HH', 'hpp', 'H'],
+    'text/x-diff':            ['diff', 'patch'],
+    'text/x-eiffel':          ['e'],
+    'text/x-elisp':           ['el'],
+    'text/x-fortran':         ['f'],
+    'text/x-haskell':         ['hs'],
+    'text/x-javascript':      ['js'],
+    'text/x-objc':            ['m', 'mm'],
+    'text/x-makefile':        ['make', 'mk',
+                               'Makefile', 'makefile', 'GNUMakefile'],
+    'text/x-pascal':          ['pas'],
+    'text/x-perl':            ['pl', 'pm', 'PL', 'perl'],
+    'text/x-php':             ['php', 'php3', 'php4'],
+    'text/x-python':          ['py', 'python'],
+    'text/x-pyrex':           ['pyx'],
+    'text/x-ruby':            ['rb', 'ruby'],
+    'text/x-scheme':          ['scm'],
+    'text/x-textile':         ['txtl', 'textile'],
+    'text/x-vba':             ['vb', 'vba', 'bas'],
+    'text/x-verilog':         ['v', 'verilog'],
+    'text/x-vhdl':            ['vhd'],
+}
+
+# extend the above with simple (text/x-<something>: <something>) mappings
+
+for x in ['ada', 'asm', 'asp', 'awk', 'idl', 'inf', 'java', 'ksh', 'lua',
+          'm4', 'mail', 'psp', 'rfc', 'rst', 'sql', 'tcl', 'tex', 'zsh']:
+    KNOWN_MIME_TYPES.setdefault('text/x-%s' % x, []).append(x)
+
+
+# Default mapping from keywords/extensions to known MIME types:
+
+MIME_MAP = {}
+for t, exts in KNOWN_MIME_TYPES.items():
+    MIME_MAP[t] = t
+    for e in exts:
+        MIME_MAP[e] = t
+
+# Simple builtin autodetection from the content using a regexp
+MODE_RE = re.compile(
+    r"#!(?:[/\w.-_]+/)?(\w+)|"               # look for shebang
+    r"-\*-\s*(?:mode:\s*)?([\w+-]+)\s*-\*-|" # look for Emacs' -*- mode -*-
+    r"vim:.*?syntax=(\w+)"                   # look for VIM's syntax=<n>
+    )
+
+def get_mimetype(filename, content=None, mime_map=MIME_MAP):
+    """Guess the most probable MIME type of a file with the given name.
+
+    `filename` is either a filename (the lookup will then use the suffix)
+    or some arbitrary keyword.
+    
+    `content` is either a `str` or an `unicode` string.
+    """
+    suffix = filename.split('.')[-1]
+    if suffix in mime_map:
+        # 1) mimetype from the suffix, using the `mime_map`
+        return mime_map[suffix]
+    else:
+        mimetype = None
+        try:
+            import mimetypes
+            # 2) mimetype from the suffix, using the `mimetypes` module
+            mimetype = mimetypes.guess_type(filename)[0]
+        except:
+            pass
+        if not mimetype and content:
+            match = re.search(MODE_RE, content[:1000])
+            if match:
+                mode = match.group(1) or match.group(3) or \
+                    match.group(2).lower()
+                if mode in mime_map:
+                    # 3) mimetype from the content, using the `MODE_RE`
+                    return mime_map[mode]
+            else:
+                if is_binary(content):
+                    # 4) mimetype from the content, using`is_binary`
+                    return 'application/octet-stream'
+        return mimetype
+
+def is_binary(data):
+    """Detect binary content by checking the first thousand bytes for zeroes.
+
+    Operate on either `str` or `unicode` strings.
+    """
+    if isinstance(data, str) and detect_unicode(data):
+        return False
+    return '\0' in data[:1000]
+
+def detect_unicode(data):
+    """Detect different unicode charsets by looking for BOMs (Byte Order Marks).
+
+    Operate obviously only on `str` objects.
+    """
+    if data.startswith('\xff\xfe'):
+        return 'utf-16-le'
+    elif data.startswith('\xfe\xff'):
+        return 'utf-16-be'
+    elif data.startswith('\xef\xbb\xbf'):
+        return 'utf-8'
+    else:
+        return None
+
+def content_to_unicode(env, content, mimetype):
+    """Retrieve an `unicode` object from a `content` to be previewed"""
+    mimeview = Mimeview(env)
+    if hasattr(content, 'read'):
+        content = content.read(mimeview.max_preview_size)
+    return mimeview.to_unicode(content, mimetype)
+
+
+class IHTMLPreviewRenderer(Interface):
+    """Extension point interface for components that add HTML renderers of
+    specific content types to the `Mimeview` component.
+
+    (Deprecated)
+    """
+
+    # implementing classes should set this property to True if they
+    # support text content where Trac should expand tabs into spaces
+    expand_tabs = False
+
+    def get_quality_ratio(mimetype):
+        """Return the level of support this renderer provides for the `content`
+        of the specified MIME type. The return value must be a number between
+        0 and 9, where 0 means no support and 9 means "perfect" support.
+        """
+
+    def render(req, mimetype, content, filename=None, url=None):
+        """Render an XHTML preview of the raw `content`.
+
+        The `content` might be:
+         * a `str` object
+         * an `unicode` string
+         * any object with a `read` method, returning one of the above
+
+        It is assumed that the content will correspond to the given `mimetype`.
+
+        Besides the `content` value, the same content may eventually
+        be available through the `filename` or `url` parameters.
+        This is useful for renderers that embed objects, using <object> or
+        <img> instead of including the content inline.
+        
+        Can return the generated XHTML text as a single string or as an
+        iterable that yields strings. In the latter case, the list will
+        be considered to correspond to lines of text in the original content.
+        """
+
+class IHTMLPreviewAnnotator(Interface):
+    """Extension point interface for components that can annotate an XHTML
+    representation of file contents with additional information."""
+
+    def get_annotation_type():
+        """Return a (type, label, description) tuple that defines the type of
+        annotation and provides human readable names. The `type` element should
+        be unique to the annotator. The `label` element is used as column
+        heading for the table, while `description` is used as a display name to
+        let the user toggle the appearance of the annotation type.
+        """
+
+    def annotate_line(number, content):
+        """Return the XHTML markup for the table cell that contains the
+        annotation data."""
+
+
+class IContentConverter(Interface):
+    """An extension point interface for generic MIME based content
+    conversion."""
+
+    def get_supported_conversions():
+        """Return an iterable of tuples in the form (key, name, extension,
+        in_mimetype, out_mimetype, quality) representing the MIME conversions
+        supported and
+        the quality ratio of the conversion in the range 0 to 9, where 0 means
+        no support and 9 means "perfect" support. eg. ('latex', 'LaTeX', 'tex',
+        'text/x-trac-wiki', 'text/plain', 8)"""
+
+    def convert_content(req, mimetype, content, key):
+        """Convert the given content from mimetype to the output MIME type
+        represented by key. Returns a tuple in the form (content,
+        output_mime_type) or None if conversion is not possible."""
+
+
+class Mimeview(Component):
+    """A generic class to prettify data, typically source code."""
+
+    renderers = ExtensionPoint(IHTMLPreviewRenderer)
+    annotators = ExtensionPoint(IHTMLPreviewAnnotator)
+    converters = ExtensionPoint(IContentConverter)
+
+    default_charset = Option('trac', 'default_charset', 'iso-8859-15',
+        """Charset to be used when in doubt.""")
+
+    tab_width = IntOption('mimeviewer', 'tab_width', 8,
+        """Displayed tab width in file preview (''since 0.9'').""")
+
+    max_preview_size = IntOption('mimeviewer', 'max_preview_size', 262144,
+        """Maximum file size for HTML preview. (''since 0.9'').""")
+
+    mime_map = ListOption('mimeviewer', 'mime_map',
+        'text/x-dylan:dylan,text/x-idl:ice,text/x-ada:ads:adb',
+        """List of additional MIME types and keyword mappings.
+        Mappings are comma-separated, and for each MIME type,
+        there's a colon (":") separated list of associated keywords
+        or file extensions. (''since 0.10'').""")
+
+    def __init__(self):
+        self._mime_map = None
+        
+    # Public API
+
+    def get_supported_conversions(self, mimetype):
+        """Return a list of target MIME types in same form as
+        `IContentConverter.get_supported_conversions()`, but with the converter
+        component appended. Output is ordered from best to worst quality."""
+        converters = []
+        for converter in self.converters:
+            for k, n, e, im, om, q in converter.get_supported_conversions():
+                if im == mimetype and q > 0:
+                    converters.append((k, n, e, im, om, q, converter))
+        converters = sorted(converters, key=lambda i: i[-1], reverse=True)
+        return converters
+
+    def convert_content(self, req, mimetype, content, key, filename=None,
+                        url=None):
+        """Convert the given content to the target MIME type represented by
+        `key`, which can be either a MIME type or a key. Returns a tuple of
+        (content, output_mime_type, extension)."""
+        if not content:
+            return ('', 'text/plain;charset=utf-8')
+
+        # Ensure we have a MIME type for this content
+        full_mimetype = mimetype
+        if not full_mimetype:
+            if hasattr(content, 'read'):
+                content = content.read(self.max_preview_size)
+            full_mimetype = self.get_mimetype(filename, content)
+        if full_mimetype:
+            mimetype = full_mimetype.split(';')[0].strip() # split off charset
+        else:
+            mimetype = full_mimetype = 'text/plain' # fallback if not binary
+
+        # Choose best converter
+        candidates = list(self.get_supported_conversions(mimetype))
+        candidates = [c for c in candidates if key in (c[0], c[4])]
+        if not candidates:
+            raise TracError('No available MIME conversions from %s to %s' %
+                            (mimetype, key))
+
+        # First successful conversion wins
+        for ck, name, ext, input_mimettype, output_mimetype, quality, \
+                converter in candidates:
+            output = converter.convert_content(req, mimetype, content, ck)
+            if not output:
+                continue
+            return (output[0], output[1], ext)
+        raise TracError('No available MIME conversions from %s to %s' %
+                        (mimetype, key))
+
+    def get_annotation_types(self):
+        """Generator that returns all available annotation types."""
+        for annotator in self.annotators:
+            yield annotator.get_annotation_type()
+
+    def render(self, req, mimetype, content, filename=None, url=None,
+               annotations=None):
+        """Render an XHTML preview of the given `content`.
+
+        `content` is the same as an `IHTMLPreviewRenderer.render`'s
+        `content` argument.
+
+        The specified `mimetype` will be used to select the most appropriate
+        `IHTMLPreviewRenderer` implementation available for this MIME type.
+        If not given, the MIME type will be infered from the filename or the
+        content.
+
+        Return a string containing the XHTML text.
+        """
+        if not content:
+            return ''
+
+        # Ensure we have a MIME type for this content
+        full_mimetype = mimetype
+        if not full_mimetype:
+            if hasattr(content, 'read'):
+                content = content.read(self.max_preview_size)
+            full_mimetype = self.get_mimetype(filename, content)
+        if full_mimetype:
+            mimetype = full_mimetype.split(';')[0].strip() # split off charset
+        else:
+            mimetype = full_mimetype = 'text/plain' # fallback if not binary
+
+        # Determine candidate `IHTMLPreviewRenderer`s
+        candidates = []
+        for renderer in self.renderers:
+            qr = renderer.get_quality_ratio(mimetype)
+            if qr > 0:
+                candidates.append((qr, renderer))
+        candidates.sort(lambda x,y: cmp(y[0], x[0]))
+
+        # First candidate which renders successfully wins.
+        # Also, we don't want to expand tabs more than once.
+        expanded_content = None
+        for qr, renderer in candidates:
+            try:
+                self.log.debug('Trying to render HTML preview using %s'
+                               % renderer.__class__.__name__)
+                # check if we need to perform a tab expansion
+                rendered_content = content
+                if getattr(renderer, 'expand_tabs', False):
+                    if expanded_content is None:
+                        content = content_to_unicode(self.env, content,
+                                                     full_mimetype)
+                        expanded_content = content.expandtabs(self.tab_width)
+                    rendered_content = expanded_content
+                result = renderer.render(req, full_mimetype, rendered_content,
+                                         filename, url)
+                if not result:
+                    continue
+                elif isinstance(result, Fragment):
+                    return result
+                elif isinstance(result, basestring):
+                    return Markup(to_unicode(result))
+                elif annotations:
+                    return Markup(self._annotate(result, annotations))
+                else:
+                    buf = StringIO()
+                    buf.write('<div class="code"><pre>')
+                    for line in result:
+                        buf.write(line + '\n')
+                    buf.write('</pre></div>')
+                    return Markup(buf.getvalue())
+            except Exception, e:
+                self.log.warning('HTML preview using %s failed (%s)'
+                                 % (renderer, e), exc_info=True)
+
+    def _annotate(self, lines, annotations):
+        buf = StringIO()
+        buf.write('<table class="code"><thead><tr>')
+        annotators = []
+        for annotator in self.annotators:
+            atype, alabel, adesc = annotator.get_annotation_type()
+            if atype in annotations:
+                buf.write('<th class="%s">%s</th>' % (atype, alabel))
+                annotators.append(annotator)
+        buf.write('<th class="content">&nbsp;</th>')
+        buf.write('</tr></thead><tbody>')
+
+        space_re = re.compile('(?P<spaces> (?: +))|'
+                              '^(?P<tag><\w+.*?>)?( )')
+        def htmlify(match):
+            m = match.group('spaces')
+            if m:
+                div, mod = divmod(len(m), 2)
+                return div * '&nbsp; ' + mod * '&nbsp;'
+            return (match.group('tag') or '') + '&nbsp;'
+
+        num = -1
+        for num, line in enumerate(_html_splitlines(lines)):
+            cells = []
+            for annotator in annotators:
+                cells.append(annotator.annotate_line(num + 1, line))
+            cells.append('<td>%s</td>\n' % space_re.sub(htmlify, line))
+            buf.write('<tr>' + '\n'.join(cells) + '</tr>')
+        else:
+            if num < 0:
+                return ''
+        buf.write('</tbody></table>')
+        return buf.getvalue()
+
+    def get_max_preview_size(self):
+        """Deprecated: use `max_preview_size` attribute directly."""
+        return self.max_preview_size
+
+    def get_charset(self, content='', mimetype=None):
+        """Infer the character encoding from the `content` or the `mimetype`.
+
+        `content` is either a `str` or an `unicode` object.
+        
+        The charset will be determined using this order:
+         * from the charset information present in the `mimetype` argument
+         * auto-detection of the charset from the `content`
+         * the configured `default_charset` 
+        """
+        if mimetype:
+            ctpos = mimetype.find('charset=')
+            if ctpos >= 0:
+                return mimetype[ctpos + 8:].strip()
+        if isinstance(content, str):
+            utf = detect_unicode(content)
+            if utf is not None:
+                return utf
+        return self.default_charset
+
+    def get_mimetype(self, filename, content=None):
+        """Infer the MIME type from the `filename` or the `content`.
+
+        `content` is either a `str` or an `unicode` object.
+
+        Return the detected MIME type, augmented by the
+        charset information (i.e. "<mimetype>; charset=..."),
+        or `None` if detection failed.
+        """
+        # Extend default extension to MIME type mappings with configured ones
+        if not self._mime_map:
+            self._mime_map = MIME_MAP
+            for mapping in self.config['mimeviewer'].getlist('mime_map'):
+                if ':' in mapping:
+                    assocations = mapping.split(':')
+                    for keyword in assocations: # Note: [0] kept on purpose
+                        self._mime_map[keyword] = assocations[0]
+
+        mimetype = get_mimetype(filename, content, self._mime_map)
+        charset = None
+        if mimetype:
+            charset = self.get_charset(content, mimetype)
+        if mimetype and charset and not 'charset' in mimetype:
+            mimetype += '; charset=' + charset
+        return mimetype
+
+    def to_utf8(self, content, mimetype=None):
+        """Convert an encoded `content` to utf-8.
+
+        ''Deprecated in 0.10. You should use `unicode` strings only.''
+        """
+        return to_utf8(content, self.get_charset(content, mimetype))
+
+    def to_unicode(self, content, mimetype=None, charset=None):
+        """Convert `content` (an encoded `str` object) to an `unicode` object.
+
+        This calls `trac.util.to_unicode` with the `charset` provided,
+        or the one obtained by `Mimeview.get_charset()`.
+        """
+        if not charset:
+            charset = self.get_charset(content, mimetype)
+        return to_unicode(content, charset)
+
+    def configured_modes_mapping(self, renderer):
+        """Return a MIME type to `(mode,quality)` mapping for given `option`"""
+        types, option = {}, '%s_modes' % renderer
+        for mapping in self.config['mimeviewer'].getlist(option):
+            if not mapping:
+                continue
+            try:
+                mimetype, mode, quality = mapping.split(':')
+                types[mimetype] = (mode, int(quality))
+            except (TypeError, ValueError):
+                self.log.warning("Invalid mapping '%s' specified in '%s' "
+                                 "option." % (mapping, option))
+        return types
+    
+    def preview_to_hdf(self, req, content, length, mimetype, filename,
+                       url=None, annotations=None):
+        """Prepares a rendered preview of the given `content`.
+
+        Note: `content` will usually be an object with a `read` method.
+        """        
+        if length >= self.max_preview_size:
+            return {'max_file_size_reached': True,
+                    'max_file_size': self.max_preview_size,
+                    'raw_href': url}
+        else:
+            return {'preview': self.render(req, mimetype, content, filename,
+                                           url, annotations),
+                    'raw_href': url}
+
+    def send_converted(self, req, in_type, content, selector, filename='file'):
+        """Helper method for converting `content` and sending it directly.
+
+        `selector` can be either a key or a MIME Type."""
+        from trac.web import RequestDone
+        content, output_type, ext = self.convert_content(req, in_type,
+                                                         content, selector)
+        req.send_response(200)
+        req.send_header('Content-Type', output_type)
+        req.send_header('Content-Disposition', 'filename=%s.%s' % (filename,
+                                                                   ext))
+        req.end_headers()
+        req.write(content)
+        raise RequestDone        
+        
+
+def _html_splitlines(lines):
+    """Tracks open and close tags in lines of HTML text and yields lines that
+    have no tags spanning more than one line."""
+    open_tag_re = re.compile(r'<(\w+)(\s.*?)?[^/]?>')
+    close_tag_re = re.compile(r'</(\w+)>')
+    open_tags = []
+    for line in lines:
+        # Reopen tags still open from the previous line
+        for tag in open_tags:
+            line = tag.group(0) + line
+        open_tags = []
+
+        # Find all tags opened on this line
+        for tag in open_tag_re.finditer(line):
+            open_tags.append(tag)
+
+        open_tags.reverse()
+
+        # Find all tags closed on this line
+        for ctag in close_tag_re.finditer(line):
+            for otag in open_tags:
+                if otag.group(1) == ctag.group(1):
+                    open_tags.remove(otag)
+                    break
+
+        # Close all tags still open at the end of line, they'll get reopened at
+        # the beginning of the next line
+        for tag in open_tags:
+            line += '</%s>' % tag.group(1)
+
+        yield line
+
+
+# -- Default annotators
+
+class LineNumberAnnotator(Component):
+    """Text annotator that adds a column with line numbers."""
+    implements(IHTMLPreviewAnnotator)
+
+    # ITextAnnotator methods
+
+    def get_annotation_type(self):
+        return 'lineno', 'Line', 'Line numbers'
+
+    def annotate_line(self, number, content):
+        return '<th id="L%s"><a href="#L%s">%s</a></th>' % (number, number,
+                                                            number)
+
+
+# -- Default renderers
+
+class PlainTextRenderer(Component):
+    """HTML preview renderer for plain text, and fallback for any kind of text
+    for which no more specific renderer is available.
+    """
+    implements(IHTMLPreviewRenderer)
+
+    expand_tabs = True
+
+    TREAT_AS_BINARY = [
+        'application/pdf',
+        'application/postscript',
+        'application/rtf'
+    ]
+
+    def get_quality_ratio(self, mimetype):
+        if mimetype in self.TREAT_AS_BINARY:
+            return 0
+        return 1
+
+    def render(self, req, mimetype, content, filename=None, url=None):
+        if is_binary(content):
+            self.env.log.debug("Binary data; no preview available")
+            return
+
+        self.env.log.debug("Using default plain text mimeviewer")
+        content = content_to_unicode(self.env, content, mimetype)
+        for line in content.splitlines():
+            yield escape(line)
+
+
+class ImageRenderer(Component):
+    """Inline image display. Here we don't need the `content` at all."""
+    implements(IHTMLPreviewRenderer)
+
+    def get_quality_ratio(self, mimetype):
+        if mimetype.startswith('image/'):
+            return 8
+        return 0
+
+    def render(self, req, mimetype, content, filename=None, url=None):
+        if url:
+            return html.DIV(html.IMG(src=url,alt=filename),
+                            class_="image-file")
+
+
+class WikiTextRenderer(Component):
+    """Render files containing Trac's own Wiki formatting markup."""
+    implements(IHTMLPreviewRenderer)
+
+    def get_quality_ratio(self, mimetype):
+        if mimetype in ('text/x-trac-wiki', 'application/x-trac-wiki'):
+            return 8
+        return 0
+
+    def render(self, req, mimetype, content, filename=None, url=None):
+        from trac.wiki import wiki_to_html
+        return wiki_to_html(content_to_unicode(self.env, content, mimetype),
+                            self.env, req)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/mimeview/enscript.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2005 Edgewall Software
+# Copyright (C) 2004 Daniel Lundin <daniel@edgewall.com>
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Daniel Lundin <daniel@edgewall.com>
+
+from trac.config import Option, ListOption
+from trac.core import *
+from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview
+from trac.util import NaivePopen
+from trac.util.markup import escape, Deuglifier
+
+__all__ = ['EnscriptRenderer']
+
+types = {
+    'application/xhtml+xml':    ('html', 2),
+    'application/postscript':   ('postscript', 2),
+    'application/x-csh':        ('csh', 2),
+    'application/x-javascript': ('javascript', 2),
+    'application/x-troff':      ('nroff', 2),
+    'text/html':                ('html', 2),
+    'text/x-ada':               ('ada', 2),
+    'text/x-asm':               ('asm', 2),
+    'text/x-awk':               ('awk', 2),
+    'text/x-c++src':            ('cpp', 2),
+    'text/x-c++hdr':            ('cpp', 2),
+    'text/x-chdr':              ('c', 2),
+    'text/x-csh':               ('csh', 2),
+    'text/x-csrc':              ('c', 2),
+    'text/x-diff':              ('diffu', 2), # Assume unified diff (works otherwise)
+    'text/x-eiffel':            ('eiffel', 2),
+    'text/x-elisp':             ('elisp', 2),
+    'text/x-fortran':           ('fortran', 2),
+    'text/x-haskell':           ('haskell', 2),
+    'text/x-idl':               ('idl', 2),
+    'text/x-inf':               ('inf', 2),
+    'text/x-java':              ('java', 2),
+    'text/x-javascript':        ('javascript', 2),
+    'text/x-ksh':               ('ksh', 2),
+    'text/x-lua':               ('lua', 2),
+    'text/x-m4':                ('m4', 2),
+    'text/x-makefile':          ('makefile', 2),
+    'text/x-mail':              ('mail', 2),
+    'text/x-matlab':            ('matlab', 2),
+    'text/x-objc':              ('objc', 2),
+    'text/x-pascal':            ('pascal', 2),
+    'text/x-perl':              ('perl', 2),
+    'text/x-pyrex':             ('pyrex', 2),
+    'text/x-python':            ('python', 2),
+    'text/x-rfc':               ('rfc', 2),
+    'text/x-ruby':              ('ruby', 2),
+    'text/x-sh':                ('sh', 2),
+    'text/x-scheme':            ('scheme', 2),
+    'text/x-sql':               ('sql', 2),
+    'text/x-tcl':               ('tcl', 2),
+    'text/x-tex':               ('tex', 2),
+    'text/x-vba':               ('vba', 2),
+    'text/x-verilog':           ('verilog', 2),
+    'text/x-vhdl':              ('vhdl', 2),
+    'model/vrml':               ('vrml', 2),
+    'application/x-sh':         ('sh', 2),
+    'text/x-zsh':               ('zsh', 2),
+    'text/vnd.wap.wmlscript':   ('wmlscript', 2),
+}
+
+
+class EnscriptDeuglifier(Deuglifier):
+    def rules(cls):
+        return [
+            r'(?P<comment><FONT COLOR="#B22222">)',
+            r'(?P<keyword><FONT COLOR="#5F9EA0">)',
+            r'(?P<type><FONT COLOR="#228B22">)',
+            r'(?P<string><FONT COLOR="#BC8F8F">)',
+            r'(?P<func><FONT COLOR="#0000FF">)',
+            r'(?P<prep><FONT COLOR="#B8860B">)',
+            r'(?P<lang><FONT COLOR="#A020F0">)',
+            r'(?P<var><FONT COLOR="#DA70D6">)',
+            r'(?P<font><FONT.*?>)',
+            r'(?P<endfont></FONT>)'
+        ]
+    rules = classmethod(rules)
+
+
+class EnscriptRenderer(Component):
+    """Syntax highlighting using GNU Enscript."""
+
+    implements(IHTMLPreviewRenderer)
+
+    expand_tabs = True
+
+    path = Option('mimeviewer', 'enscript_path', 'enscript',
+        """Path to the Enscript executable.""")
+
+    enscript_modes = ListOption('mimeviewer', 'enscript_modes',
+        'text/x-dylan:dylan:4',
+        """List of additional MIME types known by Enscript.
+        For each, a tuple `mimetype:mode:quality` has to be
+        specified, where `mimetype` is the MIME type,
+        `mode` is the corresponding Enscript mode to be used
+        for the conversion and `quality` is the quality ratio
+        associated to this conversion.
+        That can also be used to override the default
+        quality ratio used by the Enscript render, which is 2
+        (''since 0.10'').""")
+
+    def __init__(self):
+        self._types = None
+
+    # IHTMLPreviewRenderer methods
+
+    def get_quality_ratio(self, mimetype):
+        # Extend default MIME type to mode mappings with configured ones
+        if not self._types:
+            self._types = {}
+            self._types.update(types)
+            self._types.update(
+                Mimeview(self.env).configured_modes_mapping('enscript'))
+        return self._types.get(mimetype, (None, 0))[1]
+
+    def render(self, req, mimetype, content, filename=None, rev=None):
+        cmdline = self.path
+        mimetype = mimetype.split(';', 1)[0] # strip off charset
+        mode = self._types[mimetype][0]
+        cmdline += ' --color -h -q --language=html -p - -E%s' % mode
+        self.env.log.debug("Enscript command line: %s" % cmdline)
+
+        np = NaivePopen(cmdline, content.encode('utf-8'), capturestderr=1)
+        if np.errorlevel or np.err:
+            err = 'Running (%s) failed: %s, %s.' % (cmdline, np.errorlevel,
+                                                    np.err)
+            raise Exception, err
+        odata = np.out
+
+        # Strip header and footer
+        i = odata.find('<PRE>')
+        beg = i > 0 and i + 6
+        i = odata.rfind('</PRE>')
+        end = i > 0 and i or len(odata)
+
+        odata = EnscriptDeuglifier().format(odata[beg:end].decode('utf-8'))
+        return odata.splitlines()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/mimeview/patch.py
@@ -0,0 +1,204 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+#         Ludvig Strigeus
+
+from trac.core import *
+from trac.mimeview.api import content_to_unicode, IHTMLPreviewRenderer, Mimeview
+from trac.util.markup import escape, Markup
+from trac.web.chrome import add_stylesheet
+
+__all__ = ['PatchRenderer']
+
+
+class PatchRenderer(Component):
+    """Structured display of patches in unified diff format, similar to the
+    layout provided by the changeset view.
+    """
+
+    implements(IHTMLPreviewRenderer)
+
+    diff_cs = """
+<?cs include:'macros.cs' ?>
+<div class="diff"><ul class="entries"><?cs
+ each:file = diff.files ?><li class="entry">
+  <h2><?cs var:file.filename ?></h2>
+  <table class="inline" summary="Differences" cellspacing="0">
+   <colgroup><col class="lineno" /><col class="lineno" /><col class="content" /></colgroup>
+   <thead><tr>
+    <th><?cs var:file.oldrev ?></th>
+    <th><?cs var:file.newrev ?></th>
+    <th>&nbsp;</th>
+   </tr></thead><?cs
+   each:change = file.diff ?><?cs
+    call:diff_display(change, diff.style) ?><?cs
+    if:name(change) < len(file.diff) - 1 ?>
+     <tbody class="skipped">
+      <tr><th>&hellip;</th><th>&hellip;</th><td>&nbsp;</td></tr>
+     </tbody><?cs
+    /if ?><?cs
+   /each ?>
+  </table>
+ </li><?cs /each ?>
+</ul></div>
+""" # diff_cs
+
+    # IHTMLPreviewRenderer methods
+
+    def get_quality_ratio(self, mimetype):
+        if mimetype == 'text/x-diff':
+            return 8
+        return 0
+
+    def render(self, req, mimetype, content, filename=None, rev=None):
+        from trac.web.clearsilver import HDFWrapper
+
+        content = content_to_unicode(self.env, content, mimetype)
+        d = self._diff_to_hdf(content.splitlines(),
+                              Mimeview(self.env).tab_width)
+        if not d:
+            raise TracError, 'Invalid unified diff content'
+        hdf = HDFWrapper(loadpaths=[self.env.get_templates_dir(),
+                                    self.config.get('trac', 'templates_dir')])
+        hdf['diff.files'] = d
+
+        add_stylesheet(req, 'common/css/diff.css')
+        return hdf.render(hdf.parse(self.diff_cs))
+
+    # Internal methods
+
+    # FIXME: This function should probably share more code with the
+    #        trac.versioncontrol.diff module
+    def _diff_to_hdf(self, difflines, tabwidth):
+        """
+        Translate a diff file into something suitable for inclusion in HDF.
+        The result is [(filename, revname_old, revname_new, changes)],
+        where changes has the same format as the result of
+        `trac.versioncontrol.diff.hdf_diff`.
+
+        If the diff cannot be parsed, this method returns None.
+        """
+        def _markup_intraline_change(fromlines, tolines):
+            from trac.versioncontrol.diff import _get_change_extent
+            for i in xrange(len(fromlines)):
+                fr, to = fromlines[i], tolines[i]
+                (start, end) = _get_change_extent(fr, to)
+                if start != 0 and end != 0:
+                    fromlines[i] = fr[:start] + '\0' + fr[start:end+len(fr)] + \
+                                   '\1' + fr[end:]
+                    tolines[i] = to[:start] + '\0' + to[start:end+len(to)] + \
+                                 '\1' + to[end:]
+
+        import re
+        space_re = re.compile(' ( +)|^ ')
+        def htmlify(match):
+            div, mod = divmod(len(match.group(0)), 2)
+            return div * '&nbsp; ' + mod * '&nbsp;'
+
+        output = []
+        filename, groups = None, None
+        lines = iter(difflines)
+        try:
+            line = lines.next()
+            while True:
+                if not line.startswith('--- '):
+                    line = lines.next()
+                    continue
+
+                # Base filename/version
+                words = line.split(None, 2)
+                filename, fromrev = words[1], 'old'
+                groups, blocks = None, None
+
+                # Changed filename/version
+                line = lines.next()
+                if not line.startswith('+++ '):
+                    return None
+
+                words = line.split(None, 2)
+                if len(words[1]) < len(filename):
+                    # Always use the shortest filename for display
+                    filename = words[1]
+                groups = []
+                output.append({'filename' : filename, 'oldrev' : fromrev,
+                               'newrev' : 'new', 'diff' : groups})
+
+                for line in lines:
+                    # @@ -333,10 +329,8 @@
+                    r = re.match(r'@@ -(\d+),(\d+) \+(\d+),(\d+) @@', line)
+                    if not r:
+                        break
+                    blocks = []
+                    groups.append(blocks)
+                    fromline,fromend,toline,toend = map(int, r.groups())
+                    last_type = None
+
+                    fromend += fromline
+                    toend += toline
+
+                    while fromline < fromend or toline < toend:
+                        line = lines.next()
+
+                        # First character is the command
+                        command, line = line[0], line[1:]
+                        # Make a new block?
+                        if (command == ' ') != last_type:
+                            last_type = command == ' '
+                            blocks.append({'type': last_type and 'unmod' or 'mod',
+                                           'base.offset': fromline - 1,
+                                           'base.lines': [],
+                                           'changed.offset': toline - 1,
+                                           'changed.lines': []})
+                        if command == ' ':
+                            blocks[-1]['changed.lines'].append(line)
+                            blocks[-1]['base.lines'].append(line)
+                            fromline += 1
+                            toline += 1
+                        elif command == '+':
+                            blocks[-1]['changed.lines'].append(line)
+                            toline += 1
+                        elif command == '-':
+                            blocks[-1]['base.lines'].append(line)
+                            fromline += 1
+                        else:
+                            return None
+                line = lines.next()
+        except StopIteration:
+            pass
+
+        # Go through all groups/blocks and mark up intraline changes, and
+        # convert to html
+        for o in output:
+            for group in o['diff']:
+                for b in group:
+                    f, t = b['base.lines'], b['changed.lines']
+                    if b['type'] == 'mod':
+                        if len(f) == 0:
+                            b['type'] = 'add'
+                        elif len(t) == 0:
+                            b['type'] = 'rem'
+                        elif len(f) == len(t):
+                            _markup_intraline_change(f, t)
+                    for i in xrange(len(f)):
+                        line = f[i].expandtabs(tabwidth)
+                        line = escape(line).replace('\0', '<del>') \
+                                           .replace('\1', '</del>')
+                        f[i] = Markup(space_re.sub(htmlify, line))
+                    for i in xrange(len(t)):
+                        line = t[i].expandtabs(tabwidth)
+                        line = escape(line).replace('\0', '<ins>') \
+                                           .replace('\1', '</ins>')
+                        t[i] = Markup(space_re.sub(htmlify, line))
+        return output
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/mimeview/php.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christian Boos <cboos@bct-technology.com>
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christian Boos <cboos@bct-technology.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+import re
+
+from trac.core import *
+from trac.config import Option
+from trac.mimeview.api import IHTMLPreviewRenderer, content_to_unicode
+from trac.util import NaivePopen
+from trac.util.markup import Deuglifier
+
+__all__ = ['PHPRenderer']
+
+php_types = ('text/x-php', 'application/x-httpd-php',
+             'application/x-httpd-php4', 'application/x-httpd-php1')
+
+
+class PhpDeuglifier(Deuglifier):
+
+    def format(self, indata):
+        # The PHP highlighter produces the end-span tags on the next line
+        # instead of the line they actually apply to, which causes
+        # Trac to produce lots of (useless) open-and-immediately-close
+        # spans beginning each line.  This tries to curtail by bubbling
+        # the first span after a set of 1+ "<br />" to before them.
+        r_fixeol = re.compile(r"((?:<br />)+)(</(?:font|span)>)")
+        indata = r_fixeol.sub(lambda m: m.group(2) + m.group(1), indata)
+        
+        # Now call superclass implementation that handles the dirty work
+        # of applying css classes.
+        return Deuglifier.format(self, indata)
+
+    def rules(cls):
+        colors = dict(comment='FF8000', lang='0000BB', keyword='007700',
+                      string='DD0000')
+        # rules check for <font> for PHP 4 or <span> for PHP 5
+        color_rules = [
+            r'(?P<%s><(?:font color="|span style="color: )#%s">)' % c
+            for c in colors.items()
+            ]
+        return color_rules + [ r'(?P<font><font.*?>)', r'(?P<endfont></font>)' ]
+    rules = classmethod(rules)
+
+class PHPRenderer(Component):
+    """
+    Syntax highlighting using the PHP executable if available.
+    """
+
+    implements(IHTMLPreviewRenderer)
+
+    path = Option('mimeviewer', 'php_path', 'php',
+        """Path to the PHP executable (''since 0.9'').""")
+
+    # IHTMLPreviewRenderer methods
+
+    def get_quality_ratio(self, mimetype):
+        if mimetype in php_types:
+            return 4
+        return 0
+
+    def render(self, req, mimetype, content, filename=None, rev=None):
+        cmdline = self.config.get('mimeviewer', 'php_path')
+        # -n to ignore php.ini so we're using default colors
+        cmdline += ' -sn'
+        self.env.log.debug("PHP command line: %s" % cmdline)
+        
+        content = content_to_unicode(self.env, content, mimetype)
+        content = content.encode('utf-8')
+        np = NaivePopen(cmdline, content, capturestderr=1)
+        if np.errorlevel or np.err:
+            err = 'Running (%s) failed: %s, %s.' % (cmdline, np.errorlevel,
+                                                    np.err)
+            raise Exception, err
+        odata = ''.join(np.out.splitlines()[1:-2])
+        if odata.startswith('X-Powered-By'):
+            raise TracError, 'You appear to be using the PHP CGI binary.  ' \
+                             'Trac requires the CLI version for syntax ' \
+                             'highlighting.'
+
+        html = PhpDeuglifier().format(odata.decode('utf-8'))
+        for line in html.split('<br />'):
+            # PHP generates _way_ too many non-breaking spaces...
+            # We don't need them anyway, so replace them by normal spaces
+            yield line.replace('&nbsp;', ' ')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/mimeview/rst.py
@@ -0,0 +1,231 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2005 Edgewall Software
+# Copyright (C) 2004 Oliver Rutherfurd
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Daniel Lundin
+#         Oliver Rutherfurd (initial implementation)
+#         Nuutti Kotivuori (role support)
+#
+# Trac support for reStructured Text, including a custom 'trac' directive
+#
+# 'trac' directive code by Oliver Rutherfurd.
+#
+# Inserts `reference` nodes for TracLinks into the document tree.
+
+__docformat__ = 'reStructuredText'
+
+from distutils.version import StrictVersion
+import re
+
+from trac.core import *
+from trac.mimeview.api import IHTMLPreviewRenderer, content_to_unicode
+from trac.web.href import Href
+from trac.wiki.formatter import WikiProcessor
+from trac.wiki import WikiSystem
+
+WIKI_LINK = re.compile(r'(?:wiki:)?(.+)')
+TICKET_LINK = re.compile(r'(?:#(\d+))|(?:ticket:(\d+))')
+REPORT_LINK = re.compile(r'(?:{(\d+)})|(?:report:(\d+))')
+CHANGESET_LINK = re.compile(r'(?:\[(\d+)\])|(?:changeset:(\d+))')
+FILE_LINK = re.compile(r'(?:browser|repos|source):([^#]+)#?(.*)')
+
+def _wikipage(href, args):
+    return href.wiki(args[0])
+
+def _ticket(href, args):
+    return href.ticket(args[0])
+
+def _report(href, args):
+    return href.report(args[0])
+
+def _changeset(href, args):
+    return href.changeset(int(args[0]))
+
+def _browser(href, args):
+    path = args[0]
+    rev = len(args) == 2 and args[1] or ''
+    return href.browser(path, rev=rev)
+
+# TracLink REs and callback functions
+LINKS = [(TICKET_LINK, _ticket),
+         (REPORT_LINK, _report),
+         (CHANGESET_LINK, _changeset),
+         (FILE_LINK, _browser),
+         (WIKI_LINK, _wikipage)]
+
+class ReStructuredTextRenderer(Component):
+    """
+    Renders plain text in reStructuredText format as HTML.
+    """
+    implements(IHTMLPreviewRenderer)
+
+    def get_quality_ratio(self, mimetype):
+        if mimetype == 'text/x-rst':
+            return 8
+        return 0
+
+    def render(self, req, mimetype, content, filename=None, rev=None):
+        try:
+            from docutils import nodes
+            from docutils.core import publish_string
+            from docutils.parsers import rst
+            from docutils import __version__
+        except ImportError:
+            raise TracError, 'Docutils not found'
+        if StrictVersion(__version__) < StrictVersion('0.3.3'):
+            raise TracError, 'Docutils version >= %s required, %s found' \
+                             % ('0.3.3', __version__)
+
+        def trac_get_reference(rawtext, link, text):
+            for (pattern, function) in LINKS:
+                m = pattern.match(link)
+                if m:
+                    g = filter(None, m.groups())
+                    missing = 0
+                    if not text:
+                        text = g[0]
+                    if pattern == WIKI_LINK:
+                        pagename = re.search(r'^[^\#]+',g[0])
+                        if not WikiSystem(self.env).has_page(pagename.group()):
+                            missing = 1
+                            text = text + "?"
+                    uri = function(req.href, g)
+                    reference = nodes.reference(rawtext, text)
+                    reference['refuri']= uri
+                    if missing:
+                        reference.set_class('missing')
+                    return reference
+            return None
+
+        def trac(name, arguments, options, content, lineno,
+                 content_offset, block_text, state, state_machine):
+            """Inserts a `reference` node into the document 
+            for a given `TracLink`_, based on the content 
+            of the arguments.
+
+            Usage::
+
+              .. trac:: target [text]
+
+            ``target`` may be one of the following:
+
+              * For wiki: ``WikiName`` or ``wiki:WikiName``
+               * For tickets: ``#1`` or ``ticket:1``
+              * For reports: ``{1}`` or ``report:1``
+              * For changesets: ``[1]`` or ``changeset:1``
+              * For files: ``source:trunk/COPYING``
+
+            ``[text]`` is optional.  If not given, ``target`` is
+            used as the reference text.
+
+            .. _TracLink: http://projects.edgewall.com/trac/wiki/TracLinks
+            """
+            link = arguments[0]
+            if len(arguments) == 2:
+                text = arguments[1]
+            else:
+                text = None
+            reference = trac_get_reference(block_text, link, text)
+            if reference:
+                p = nodes.paragraph()
+                p += reference
+                return p
+            # didn't find a match (invalid TracLink),
+            # report a warning
+            warning = state_machine.reporter.warning(
+                    '%s is not a valid TracLink' % (arguments[0]),
+                    nodes.literal_block(block_text, block_text),
+                    line=lineno)
+            return [warning]
+
+        def trac_role(name, rawtext, text, lineno, inliner, options={},
+                      content=[]):
+            args  = text.split(" ",1)
+            link = args[0]
+            if len(args)==2:
+                text = args[1]
+            else:
+                text = None
+            reference = trac_get_reference(rawtext, link, text)
+            if reference:
+                return [reference], []
+            warning = nodes.warning(None, nodes.literal_block(text,
+                'WARNING: %s is not a valid TracLink' % rawtext))
+            return warning, []
+
+        # 1 required arg, 1 optional arg, spaces allowed in last arg
+        trac.arguments = (1,1,1)
+        trac.options = None
+        trac.content = None
+        rst.directives.register_directive('trac', trac)
+        rst.roles.register_local_role('trac', trac_role)
+
+        # The code_block could is taken from the leo plugin rst2
+        def code_formatter(language, text):
+            processor = WikiProcessor(self.env, language)
+            html = processor.process(req, text)
+            raw = nodes.raw('', html, format='html')
+            return raw
+        
+        def code_role(name, rawtext, text, lineno, inliner, options={},
+                      content=[]):
+            language = options.get('language')
+            if not language:
+                args  = text.split(':', 1)
+                language = args[0]
+                if len(args) == 2:
+                    text = args[1]
+                else:
+                    text = ''
+            reference = code_formatter(language, text)
+            return [reference], []
+        
+        def code_block(name, arguments, options, content, lineno,
+                       content_offset, block_text, state, state_machine):
+            """
+            Create a code-block directive for docutils.
+
+            Usage: .. code-block:: language
+
+            If the language can be syntax highlighted it will be.
+            """
+            language = arguments[0]
+            text = '\n'.join(content)        
+            reference = code_formatter(language, text)
+            return [reference]
+
+        # These are documented
+        # at http://docutils.sourceforge.net/spec/howto/rst-directives.html.
+        code_block.arguments = (
+            1, # Number of required arguments.
+            0, # Number of optional arguments.
+            0) # True if final argument may contain whitespace.
+    
+        # A mapping from option name to conversion function.
+        code_role.options = code_block.options = {
+            'language' :
+            rst.directives.unchanged # Return the text argument, unchanged
+        }
+        code_block.content = 1 # True if content is allowed.
+        # Register the directive with docutils.
+        rst.directives.register_directive('code-block', code_block)
+        rst.roles.register_local_role('code-block', code_role)
+
+        _inliner = rst.states.Inliner()
+        _parser = rst.Parser(inliner=_inliner)
+        content = content_to_unicode(self.env, content, mimetype)
+        content = content.encode('utf-8')
+        html = publish_string(content, writer_name='html', parser=_parser,
+                              settings_overrides={'halt_level': 6})
+        html = html.decode('utf-8')
+        return html[html.find('<body>') + 6:html.find('</body>')].strip()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/mimeview/silvercity.py
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2006 Edgewall Software
+# Copyright (C) 2004 Daniel Lundin <daniel@edgewall.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Daniel Lundin <daniel@edgewall.com>
+
+"""Syntax highlighting module, based on the SilverCity module.
+
+Get it at: http://silvercity.sourceforge.net/
+"""
+
+import re
+from StringIO import StringIO
+
+from trac.core import *
+from trac.config import ListOption
+from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview
+
+__all__ = ['SilverCityRenderer']
+
+types = {
+    'text/css':                 ('CSS', 3),
+    'text/html':                ('HyperText', 3, {'asp.default.language':1}),
+    'application/xml':          ('XML', 3),
+    'application/xhtml+xml':    ('HyperText', 3, {'asp.default.language':1}),
+    'application/x-javascript': ('CPP', 3), # Kludgy.
+    'text/x-asp':               ('HyperText', 3, {'asp.default.language':2}),
+    'text/x-c++hdr':            ('CPP', 3),
+    'text/x-c++src':            ('CPP', 3),
+    'text/x-chdr':              ('CPP', 3),
+    'text/x-csrc':              ('CPP', 3),
+    'text/x-perl':              ('Perl', 3),
+    'text/x-php':               ('HyperText', 3, {'asp.default.language':4}),
+    'application/x-httpd-php':  ('HyperText', 3, {'asp.default.language':4}),
+    'application/x-httpd-php4': ('HyperText', 3, {'asp.default.language':4}),
+    'application/x-httpd-php3': ('HyperText', 3, {'asp.default.language':4}),
+    'text/x-javascript':        ('CPP', 3), # Kludgy.
+    'text/x-psp':               ('HyperText', 3, {'asp.default.language':3}),
+    'text/x-python':            ('Python', 3),
+    'text/x-ruby':              ('Ruby', 3),
+    'text/x-sql':               ('SQL', 3),
+    'text/xml':                 ('XML', 3),
+    'text/xslt':                ('XSLT', 3),
+    'image/svg+xml':            ('XML', 3)
+}
+
+CRLF_RE = re.compile('\r$', re.MULTILINE)
+
+
+class SilverCityRenderer(Component):
+    """Syntax highlighting based on SilverCity."""
+
+    implements(IHTMLPreviewRenderer)
+
+    enscript_modes = ListOption('mimeviewer', 'silvercity_modes',
+        '',
+        """List of additional MIME types known by SilverCity.
+        For each, a tuple `mimetype:mode:quality` has to be
+        specified, where `mimetype` is the MIME type,
+        `mode` is the corresponding SilverCity mode to be used
+        for the conversion and `quality` is the quality ratio
+        associated to this conversion.
+        That can also be used to override the default
+        quality ratio used by the SilverCity render, which is 3
+        (''since 0.10'').""")
+
+    expand_tabs = True
+
+    def __init__(self):
+        self._types = None
+
+    def get_quality_ratio(self, mimetype):
+        # Extend default MIME type to mode mappings with configured ones
+        if not self._types:
+            self._types = {}
+            self._types.update(types)
+            self._types.update(
+                Mimeview(self.env).configured_modes_mapping('silvercity'))
+        return self._types.get(mimetype, (None, 0))[1]
+
+    def render(self, req, mimetype, content, filename=None, rev=None):
+        import SilverCity
+        try:
+            mimetype = mimetype.split(';', 1)[0]
+            typelang = self._types[mimetype]
+            lang = typelang[0]
+            module = getattr(SilverCity, lang)
+            generator = getattr(module, lang + "HTMLGenerator")
+            try:
+                allprops = typelang[2]
+                propset = SilverCity.PropertySet()
+                for p in allprops.keys():
+                    propset[p] = allprops[p]
+            except IndexError:
+                pass
+        except (KeyError, AttributeError):
+            err = "No SilverCity lexer found for mime-type '%s'." % mimetype
+            raise Exception, err
+
+        # SilverCity does not like unicode strings
+        content = content.encode('utf-8')
+        
+        # SilverCity generates extra empty line against some types of
+        # the line such as comment or #include with CRLF. So we
+        # standardize to LF end-of-line style before call.
+        content = CRLF_RE.sub('', content)
+
+        buf = StringIO()
+        generator().generate_html(buf, content)
+
+        br_re = re.compile(r'<br\s*/?>$', re.MULTILINE)
+        span_default_re = re.compile(r'<span class="\w+_default">(.*?)</span>',
+                                     re.DOTALL)
+        html = span_default_re.sub(r'\1', br_re.sub('', buf.getvalue()))
+        
+        # Convert the output back to a unicode string
+        html = html.decode('utf-8')
+
+        # SilverCity generates _way_ too many non-breaking spaces...
+        # We don't need them anyway, so replace them by normal spaces
+        return html.replace('&nbsp;', ' ').splitlines()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/mimeview/tests/__init__.py
@@ -0,0 +1,11 @@
+from trac.mimeview.tests import api
+
+import unittest
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(api.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/mimeview/tests/api.py
@@ -0,0 +1,91 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import unittest
+
+from trac.mimeview.api import get_mimetype, _html_splitlines
+
+class GetMimeTypeTestCase(unittest.TestCase):
+
+    def test_from_suffix_using_MIME_MAP(self):
+        self.assertEqual('text/plain', get_mimetype('README', None))
+        self.assertEqual('text/plain', get_mimetype('README.txt', None))
+        
+    def test_from_suffix_using_mimetypes(self):
+        self.assertEqual('application/x-python-code',
+                         get_mimetype('test.pyc', None))
+        
+    def test_from_content_using_CONTENT_RE(self):
+        self.assertEqual('text/x-python',
+                         get_mimetype('xxx', """
+#!/usr/bin/python
+# This is a python script
+"""))
+        self.assertEqual('text/x-ksh',
+                         get_mimetype('xxx', """
+#!/bin/ksh
+# This is a shell script
+"""))
+        self.assertEqual('text/x-python',
+                         get_mimetype('xxx', """
+# -*- Python -*-
+# This is a python script
+"""))
+        self.assertEqual('text/x-ruby',
+                         get_mimetype('xxx', """
+# -*- mode: ruby -*-
+# This is a ruby script
+"""))
+
+    def test_from_content_using_is_binary(self):
+        self.assertEqual('application/octet-stream',
+                         get_mimetype('xxx', "abc\0xyz"))
+        
+    
+class MimeviewTestCase(unittest.TestCase):
+
+    def test_html_splitlines_without_markup(self):
+        lines = ['line 1', 'line 2']
+        self.assertEqual(lines, list(_html_splitlines(lines)))
+
+    def test_html_splitlines_with_markup(self):
+        lines = ['<p><b>Hi', 'How are you</b></p>']
+        result = list(_html_splitlines(lines))
+        self.assertEqual('<p><b>Hi</b></p>', result[0])
+        self.assertEqual('<p><b>How are you</b></p>', result[1])
+
+    def test_html_splitlines_with_multiline(self):
+        """
+        Regression test for http://projects.edgewall.com/trac/ticket/2655
+        """
+        lines = ['<span class="p_tripledouble">"""',
+                'a <a href="http://google.com">http://google.com</a>/',
+                'Test', 'Test', '"""</span>']
+        result = list(_html_splitlines(lines))
+        self.assertEqual('<span class="p_tripledouble">"""</span>', result[0])
+        self.assertEqual('<span class="p_tripledouble">a '
+                         '<a href="http://google.com">http://google.com</a>/'
+                         '</span>', result[1])
+        self.assertEqual('<span class="p_tripledouble">Test</span>', result[2])
+        self.assertEqual('<span class="p_tripledouble">Test</span>', result[3])
+        self.assertEqual('<span class="p_tripledouble">"""</span>', result[4])
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(GetMimeTypeTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(MimeviewTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/mimeview/txtl.py
@@ -0,0 +1,36 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2006 Edgewall Software
+# Copyright (C) 2004 Daniel Lundin
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Daniel Lundin <daniel@edgewall.com>
+
+"""Trac support for Textile
+See also: http://dealmeida.net/projects/textile/
+"""
+
+from trac.core import *
+from trac.mimeview.api import IHTMLPreviewRenderer
+
+
+class TextileRenderer(Component):
+    """Renders plain text in Textile format as HTML."""
+    implements(IHTMLPreviewRenderer)
+
+    def get_quality_ratio(self, mimetype):
+        if mimetype == 'text/x-textile':
+            return 8
+        return 0
+
+    def render(self, req, mimetype, content, filename=None, rev=None):
+        import textile
+        return textile.textile(content.encode('utf-8'), encoding='utf-8')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/notification.py
@@ -0,0 +1,372 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Daniel Lundin <daniel@edgewall.com>
+# Copyright (C) 2005-2006 Emmanuel Blot <emmanuel.blot@free.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+
+import time
+import smtplib
+import re
+
+from trac import __version__
+from trac.config import BoolOption, IntOption, Option
+from trac.core import *
+from trac.util.text import CRLF, wrap
+from trac.web.chrome import Chrome
+from trac.web.clearsilver import HDFWrapper
+from trac.web.main import populate_hdf
+
+MAXHEADERLEN = 76
+
+
+class NotificationSystem(Component):
+
+    smtp_enabled = BoolOption('notification', 'smtp_enabled', 'false',
+        """Enable SMTP (email) notification.""")
+
+    smtp_server = Option('notification', 'smtp_server', 'localhost',
+        """SMTP server hostname to use for email notifications.""")
+
+    smtp_port = IntOption('notification', 'smtp_port', 25,
+        """SMTP server port to use for email notification.""")
+
+    smtp_user = Option('notification', 'smtp_user', '',
+        """Username for SMTP server. (''since 0.9'').""")
+
+    smtp_password = Option('notification', 'smtp_password', '',
+        """Password for SMTP server. (''since 0.9'').""")
+
+    smtp_from = Option('notification', 'smtp_from', 'trac@localhost',
+        """Sender address to use in notification emails.""")
+
+    smtp_replyto = Option('notification', 'smtp_replyto', 'trac@localhost',
+        """Reply-To address to use in notification emails.""")
+
+    smtp_always_cc = Option('notification', 'smtp_always_cc', '',
+        """Email address(es) to always send notifications to,
+           addresses can be see by all recipients (Cc:).""")
+
+    smtp_always_bcc = Option('notification', 'smtp_always_bcc', '',
+        """Email address(es) to always send notifications to,
+           addresses do not appear publicly (Bcc:). (''since 0.10'').""")
+           
+    smtp_default_domain = Option('notification', 'smtp_default_domain', '',
+        """Default host/domain to append to address that do not specify one""")
+           
+    mime_encoding = Option('notification', 'mime_encoding', 'base64',
+        """Specifies the MIME encoding scheme for emails.
+        
+        Valid options are 'base64' for Base64 encoding, 'qp' for
+        Quoted-Printable, and 'none' for no encoding. Note that the no encoding
+        means that non-ASCII characters in text are going to cause problems
+        with notifications (''since 0.10'').""")
+
+    use_public_cc = BoolOption('notification', 'use_public_cc', 'false',
+        """Recipients can see email addresses of other CC'ed recipients.
+        
+        If this option is disabled (the default), recipients are put on BCC
+        (''since 0.10'').""")
+
+    use_short_addr = BoolOption('notification', 'use_short_addr', 'false',
+        """Permit email address without a host/domain (i.e. username only)
+        
+        The SMTP server should accept those addresses, and either append
+        a FQDN or use local delivery (''since 0.10'').""")
+        
+    use_tls = BoolOption('notification', 'use_tls', 'false',
+        """Use SSL/TLS to send notifications (''since 0.10'').""")
+
+
+class Notify(object):
+    """Generic notification class for Trac.
+    
+    Subclass this to implement different methods.
+    """
+
+    def __init__(self, env):
+        self.env = env
+        self.config = env.config
+        self.db = env.get_db_cnx()
+
+        loadpaths = Chrome(self.env).get_all_templates_dirs()
+        self.hdf = HDFWrapper(loadpaths)
+        populate_hdf(self.hdf, env)
+
+    def notify(self, resid):
+        (torcpts, ccrcpts) = self.get_recipients(resid)
+        self.begin_send()
+        self.send(torcpts, ccrcpts)
+        self.finish_send()
+
+    def get_recipients(self, resid):
+        """Return a pair of list of subscribers to the resource 'resid'.
+        
+        First list represents the direct recipients (To:), second list
+        represents the recipients in carbon copy (Cc:).
+        """
+        raise NotImplementedError
+
+    def begin_send(self):
+        """Prepare to send messages.
+        
+        Called before sending begins.
+        """
+
+    def send(self, torcpts, ccrcpts):
+        """Send message to recipients."""
+        raise NotImplementedError
+
+    def finish_send(self):
+        """Clean up after sending all messages.
+        
+        Called after sending all messages.
+        """
+
+
+class NotifyEmail(Notify):
+    """Baseclass for notification by email."""
+
+    smtp_server = 'localhost'
+    smtp_port = 25
+    from_email = 'trac+tickets@localhost'
+    subject = ''
+    server = None
+    email_map = None
+    template_name = None
+    addrfmt = r"[\w\d_\.\-\+=]+\@(([\w\d\-])+\.)+([\w\d]{2,4})+"
+    shortaddr_re = re.compile(addrfmt)
+    longaddr_re = re.compile(r"^\s*(.*)\s+<(" + addrfmt + ")>\s*$");
+    nodomaddr_re = re.compile(r"[\w\d_\.\-]+")
+    addrsep_re = re.compile(r"[;\s,]+")
+
+    def __init__(self, env):
+        Notify.__init__(self, env)
+
+        self._use_tls = self.env.config.getbool('notification', 'use_tls')
+        self._init_pref_encoding()
+        # Get the email addresses of all known users
+        self.email_map = {}
+        for username, name, email in self.env.get_known_users(self.db):
+            if email:
+                self.email_map[username] = email
+                
+    def _init_pref_encoding(self):
+        from email.Charset import Charset, QP, BASE64
+        self._charset = Charset()
+        self._charset.input_charset = 'utf-8'
+        pref = self.env.config.get('notification', 'mime_encoding').lower()
+        if pref == 'base64':
+            self._charset.header_encoding = BASE64
+            self._charset.body_encoding = BASE64
+            self._charset.output_charset = 'utf-8'
+            self._charset.input_codec = 'utf-8'
+            self._charset.output_codec = 'utf-8'
+        elif pref in ['qp', 'quoted-printable']:
+            self._charset.header_encoding = QP
+            self._charset.body_encoding = QP
+            self._charset.output_charset = 'utf-8'
+            self._charset.input_codec = 'utf-8'
+            self._charset.output_codec = 'utf-8'
+        elif pref == 'none':
+            self._charset.header_encoding = None
+            self._charset.body_encoding = None
+            self._charset.input_codec = None
+            self._charset.output_charset = 'ascii'
+        else:
+            raise TracError, 'Invalid email encoding setting: %s' % pref
+
+    def notify(self, resid, subject):
+        self.subject = subject
+
+        if not self.config.getbool('notification', 'smtp_enabled'):
+            return
+        self.smtp_server = self.config['notification'].get('smtp_server')
+        self.smtp_port = self.config['notification'].getint('smtp_port')
+        self.from_email = self.config['notification'].get('smtp_from')
+        self.replyto_email = self.config['notification'].get('smtp_replyto')
+        self.from_email = self.from_email or self.replyto_email
+        if not self.from_email and not self.replyto_email:
+            raise TracError(Markup('Unable to send email due to identity '
+                                   'crisis.<p>Neither <b>notification.from</b> '
+                                   'nor <b>notification.reply_to</b> are '
+                                   'specified in the configuration.</p>'),
+                            'SMTP Notification Error')
+
+        # Authentication info (optional)
+        self.user_name = self.config['notification'].get('smtp_user')
+        self.password = self.config['notification'].get('smtp_password')
+
+        Notify.notify(self, resid)
+
+    def format_header(self, key, name, email=None):
+        from email.Header import Header
+        maxlength = MAXHEADERLEN-(len(key)+2)
+        # Do not sent ridiculous short headers
+        if maxlength < 10:
+            raise TracError, "Header length is too short"
+        try:
+            tmp = name.encode('ascii')
+            header = Header(tmp, 'ascii', maxlinelen=maxlength)
+        except UnicodeEncodeError:
+            header = Header(name, self._charset, maxlinelen=maxlength)
+        if not email:
+            return header
+        else:
+            return "\"%s\" <%s>" % (header, email)
+
+    def add_headers(self, msg, headers):
+        for h in headers:
+            msg[h] = self.encode_header(h, headers[h])
+
+    def get_smtp_address(self, address):
+        if not address:
+            return None
+        if address.find('@') == -1:
+            if address == 'anonymous':
+                return None
+            if self.email_map.has_key(address):
+                address = self.email_map[address]
+            elif NotifyEmail.nodomaddr_re.match(address):
+                if self.config.getbool('notification', 'use_short_addr'):
+                    return address
+                domain = self.config.get('notification', 'smtp_default_domain')
+                if domain:
+                    address = "%s@%s" % (address, domain)
+                else:
+                    self.env.log.info("Email address w/o domain: %s" % address)
+                    return None
+        mo = NotifyEmail.shortaddr_re.search(address)
+        if mo:
+            return mo.group(0)
+        mo = NotifyEmail.longaddr_re.search(address)
+        if mo:
+            return mo.group(2)
+        self.env.log.info("Invalid email address: %s" % address)
+        return None
+
+    def encode_header(self, key, value):
+        if isinstance(value, tuple):
+            return self.format_header(key, value[0], value[1])
+        if isinstance(value, list):
+            items = []
+            for v in value:
+                items.append(self.encode_header(v))
+            return ',\n\t'.join(items)
+        mo = NotifyEmail.longaddr_re.match(value)
+        if mo:
+            return self.format_header(key, mo.group(1), mo.group(2))
+        return self.format_header(key, value)
+
+    def begin_send(self):
+        self.server = smtplib.SMTP(self.smtp_server, self.smtp_port)
+        # self.server.set_debuglevel(True)
+        if self._use_tls:
+            self.server.ehlo()
+            if not self.server.esmtp_features.has_key('starttls'):
+                raise TracError, "TLS enabled but server does not support TLS"
+            self.server.starttls()
+            self.server.ehlo()
+        if self.user_name:
+            self.server.login(self.user_name, self.password)
+
+    def send(self, torcpts, ccrcpts, mime_headers={}):
+        from email.MIMEText import MIMEText
+        from email.Utils import formatdate, formataddr
+        body = self.hdf.render(self.template_name)
+        projname = self.config.get('project', 'name')
+        public_cc = self.config.getbool('notification', 'use_public_cc')
+        headers = {}
+        headers['X-Mailer'] = 'Trac %s, by Edgewall Software' % __version__
+        headers['X-Trac-Version'] =  __version__
+        headers['X-Trac-Project'] =  projname
+        headers['X-URL'] = self.config.get('project', 'url')
+        headers['Subject'] = self.subject
+        headers['From'] = (projname, self.from_email)
+        headers['Sender'] = self.from_email
+        headers['Reply-To'] = self.replyto_email
+
+        def build_addresses(rcpts):
+            """Format and remove invalid addresses"""
+            return filter(lambda x: x, \
+                          [self.get_smtp_address(addr) for addr in rcpts])
+
+        def remove_dup(rcpts, all):
+            """Remove duplicates"""
+            tmp = []
+            for rcpt in rcpts:
+                if not rcpt in all:
+                    tmp.append(rcpt)
+                    all.append(rcpt)
+            return (tmp, all)
+
+        toaddrs = build_addresses(torcpts)
+        ccaddrs = build_addresses(ccrcpts)
+        accparam = self.config.get('notification', 'smtp_always_cc')
+        accaddrs = accparam and \
+                   build_addresses(accparam.replace(',', ' ').split()) or []
+        bccparam = self.config.get('notification', 'smtp_always_bcc')
+        bccaddrs = bccparam and \
+                   build_addresses(bccparam.replace(',', ' ').split()) or []
+
+        recipients = []
+        (toaddrs, recipients) = remove_dup(toaddrs, recipients)
+        (ccaddrs, recipients) = remove_dup(ccaddrs, recipients)
+        (accaddrs, recipients) = remove_dup(accaddrs, recipients)
+        (bccaddrs, recipients) = remove_dup(bccaddrs, recipients)
+        
+        # if there is not valid recipient, leave immediately
+        if len(recipients) < 1:
+            return
+
+        pcc = accaddrs
+        if public_cc:
+            pcc += ccaddrs
+            if toaddrs:
+                headers['To'] = ', '.join(toaddrs)
+        if pcc:
+            headers['Cc'] = ', '.join(pcc)
+        headers['Date'] = formatdate()
+        # sanity check
+        if not self._charset.body_encoding:
+            try:
+                dummy = body.encode('ascii')
+            except UnicodeDecodeError:
+                raise TracError, "Ticket contains non-Ascii chars. " \
+                                 "Please change encoding setting"
+        msg = MIMEText(body, 'plain')
+        # Message class computes the wrong type from MIMEText constructor,
+        # which does not take a Charset object as initializer. Reset the
+        # encoding type to force a new, valid evaluation
+        del msg['Content-Transfer-Encoding']
+        msg.set_charset(self._charset)
+        self.add_headers(msg, headers);
+        self.add_headers(msg, mime_headers);
+        self.env.log.debug("Sending SMTP notification to %s on port %d to %s"
+                           % (self.smtp_server, self.smtp_port, recipients))
+        msgtext = msg.as_string()
+        # Ensure the message complies with RFC2822: use CRLF line endings
+        recrlf = re.compile("\r?\n")
+        msgtext = "\r\n".join(recrlf.split(msgtext))
+        self.server.sendmail(msg['From'], recipients, msgtext)
+
+    def finish_send(self):
+        if self._use_tls:
+            # avoid false failure detection when the server closes
+            # the SMTP connection with TLS enabled
+            import socket
+            try:
+                self.server.quit()
+            except socket.sslerror:
+                pass
+        else:
+            self.server.quit()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/perm.py
@@ -0,0 +1,290 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+"""Management of permissions."""
+
+from trac.config import ExtensionOption
+from trac.core import *
+
+__all__ = ['IPermissionRequestor', 'IPermissionStore',
+           'IPermissionGroupProvider', 'PermissionError', 'PermissionSystem']
+
+class PermissionError(StandardError):
+    """Insufficient permissions to complete the operation"""
+
+    def __init__ (self, action):
+        StandardError.__init__(self)
+        self.action = action
+
+    def __str__ (self):
+        return '%s privileges are required to perform this operation' % self.action
+
+
+class IPermissionRequestor(Interface):
+    """Extension point interface for components that define actions."""
+
+    def get_permission_actions():
+        """Return a list of actions defined by this component.
+        
+        The items in the list may either be simple strings, or
+        `(string, sequence)` tuples. The latter are considered to be "meta
+        permissions" that group several simple actions under one name for
+        convenience.
+        """
+
+
+class IPermissionStore(Interface):
+    """Extension point interface for components that provide storage and
+    management of permissions."""
+
+    def get_user_permissions(username):
+        """Return all permissions for the user with the specified name.
+        
+        The permissions are returned as a dictionary where the key is the name
+        of the permission, and the value is either `True` for granted
+        permissions or `False` for explicitly denied permissions."""
+
+    def get_all_permissions():
+        """Return all permissions for all users.
+
+        The permissions are returned as a list of (subject, action)
+        formatted tuples."""
+
+    def grant_permission(username, action):
+        """Grant a user permission to perform an action."""
+
+    def revoke_permission(username, action):
+        """Revokes the permission of the given user to perform an action."""
+
+
+class IPermissionGroupProvider(Interface):
+    """Extension point interface for components that provide information about
+    user groups.
+    """
+
+    def get_permission_groups(username):
+        """Return a list of names of the groups that the user with the specified
+        name is a member of."""
+
+
+class DefaultPermissionStore(Component):
+    """Default implementation of permission storage and simple group management.
+    
+    This component uses the `PERMISSION` table in the database to store both
+    permissions and groups.
+    """
+    implements(IPermissionStore)
+
+    group_providers = ExtensionPoint(IPermissionGroupProvider)
+
+    def get_user_permissions(self, username):
+        """Retrieve the permissions for the given user and return them in a
+        dictionary.
+        
+        The permissions are stored in the database as (username, action)
+        records. There's simple support for groups by using lowercase names for
+        the action column: such a record represents a group and not an actual
+        permission, and declares that the user is part of that group.
+        """
+        subjects = [username]
+        for provider in self.group_providers:
+            subjects += list(provider.get_permission_groups(username))
+
+        actions = []
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT username,action FROM permission")
+        rows = cursor.fetchall()
+        while True:
+            num_users = len(subjects)
+            num_actions = len(actions)
+            for user, action in rows:
+                if user in subjects:
+                    if not action.islower() and action not in actions:
+                        actions.append(action)
+                    if action.islower() and action not in subjects:
+                        # action is actually the name of the permission group
+                        # here
+                        subjects.append(action)
+            if num_users == len(subjects) and num_actions == len(actions):
+                break
+        return [action for action in actions if not action.islower()]
+
+    def get_all_permissions(self):
+        """Return all permissions for all users.
+
+        The permissions are returned as a list of (subject, action)
+        formatted tuples."""
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT username,action FROM permission")
+        return [(row[0], row[1]) for row in cursor]
+
+    def grant_permission(self, username, action):
+        """Grants a user the permission to perform the specified action."""
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("INSERT INTO permission VALUES (%s, %s)",
+                       (username, action))
+        self.log.info('Granted permission for %s to %s' % (action, username))
+        db.commit()
+
+    def revoke_permission(self, username, action):
+        """Revokes a users' permission to perform the specified action."""
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("DELETE FROM permission WHERE username=%s AND action=%s",
+                       (username, action))
+        self.log.info('Revoked permission for %s to %s' % (action, username))
+        db.commit()
+
+
+class DefaultPermissionGroupProvider(Component):
+    """Provides the basic builtin permission groups 'anonymous' and
+    'authenticated'."""
+
+    implements(IPermissionGroupProvider)
+
+    def get_permission_groups(self, username):
+        groups = ['anonymous']
+        if username and username != 'anonymous':
+            groups.append('authenticated')
+        return groups
+
+
+class PermissionSystem(Component):
+    """Sub-system that manages user permissions."""
+
+    implements(IPermissionRequestor)
+
+    requestors = ExtensionPoint(IPermissionRequestor)
+
+    store = ExtensionOption('trac', 'permission_store', IPermissionStore,
+                            'DefaultPermissionStore',
+        """Name of the component implementing `IPermissionStore`, which is used
+        for managing user and group permissions.""")
+
+    # Public API
+
+    def grant_permission(self, username, action):
+        """Grant the user with the given name permission to perform to specified
+        action."""
+        if action.isupper() and action not in self.get_actions():
+            raise TracError, '%s is not a valid action.' % action
+
+        self.store.grant_permission(username, action)
+
+    def revoke_permission(self, username, action):
+        """Revokes the permission of the specified user to perform an action."""
+        # TODO: Validate that this permission does in fact exist
+        if action.isupper() and action not in self.get_actions():
+            raise TracError, '%s is not a valid action.' % action
+
+        self.store.revoke_permission(username, action)
+
+    def get_actions(self):
+        actions = []
+        for requestor in self.requestors:
+            for action in requestor.get_permission_actions():
+                if isinstance(action, tuple):
+                    actions.append(action[0])
+                else:
+                    actions.append(action)
+        return actions
+
+    def get_user_permissions(self, username=None):
+        """Return the permissions of the specified user.
+        
+        The return value is a dictionary containing all the actions as keys, and
+        a boolean value. `True` means that the permission is granted, `False`
+        means the permission is denied."""
+        actions = []
+        for requestor in self.requestors:
+            actions += list(requestor.get_permission_actions())
+        permissions = {}
+        if username:
+            # Return all permissions that the given user has
+            meta = {}
+            for action in actions:
+                if isinstance(action, tuple):
+                    name, value = action
+                    meta[name] = value
+            def _expand_meta(action):
+                permissions[action] = True
+                if meta.has_key(action):
+                    [_expand_meta(perm) for perm in meta[action]]
+            for perm in self.store.get_user_permissions(username):
+                _expand_meta(perm)
+        else:
+            # Return all permissions available in the system
+            for action in actions:
+                if isinstance(action, tuple):
+                    permissions[action[0]] = True
+                else:
+                    permissions[action] = True
+        return permissions
+
+    def get_all_permissions(self):
+        """Return all permissions for all users.
+
+        The permissions are returned as a list of (subject, action)
+        formatted tuples."""
+        return self.store.get_all_permissions()
+
+    # IPermissionRequestor methods
+
+    def get_permission_actions(self):
+        """Implement the global `TRAC_ADMIN` meta permission."""
+        actions = []
+        for requestor in [r for r in self.requestors if r is not self]:
+            for action in requestor.get_permission_actions():
+                if isinstance(action, tuple):
+                    actions.append(action[0])
+                else:
+                    actions.append(action)
+        return [('TRAC_ADMIN', actions)]
+
+
+class PermissionCache(object):
+    """Cache that maintains the permissions of a single user."""
+
+    def __init__(self, env, username):
+        self.perms = PermissionSystem(env).get_user_permissions(username)
+
+    def has_permission(self, action):
+        return self.perms.has_key(action)
+
+    def assert_permission(self, action):
+        if not self.perms.has_key(action):
+            raise PermissionError(action)
+
+    def permissions(self):
+        return self.perms.keys()
+
+
+class NoPermissionCache(object):
+    """Permission cache for ''anonymous requests''."""
+
+    def has_permission(self, action):
+        return False
+
+    def assert_permission(self, action):
+        raise PermissionError(action)
+
+    def permissions(self):
+        return []
new file mode 100644
new file mode 100755
--- /dev/null
+++ b/examples/trac/trac/scripts/admin.py
@@ -0,0 +1,1206 @@
+# -*- coding: utf-8 -*-
+# 
+# Copyright (C) 2003-2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+
+__copyright__ = 'Copyright (c) 2003-2006 Edgewall Software'
+
+import cmd
+import getpass
+import os
+import shlex
+import shutil
+import StringIO
+import sys
+import time
+import traceback
+import urllib
+import locale
+
+import trac
+from trac import perm, util, db_default
+from trac.config import default_dir
+from trac.core import TracError
+from trac.env import Environment
+from trac.perm import PermissionSystem
+from trac.ticket.model import *
+from trac.util.markup import html
+from trac.util.text import to_unicode, wrap
+from trac.wiki import WikiPage
+from trac.wiki.macros import WikiMacroBase
+
+def copytree(src, dst, symlinks=False, skip=[]):
+    """Recursively copy a directory tree using copy2() (from shutil.copytree.)
+
+    Added a `skip` parameter consisting of absolute paths
+    which we don't want to copy.
+    """
+    names = os.listdir(src)
+    os.mkdir(dst)
+    errors = []
+    for name in names:
+        srcname = os.path.join(src, name)
+        if srcname in skip:
+            continue
+        dstname = os.path.join(dst, name)
+        try:
+            if symlinks and os.path.islink(srcname):
+                linkto = os.readlink(srcname)
+                os.symlink(linkto, dstname)
+            elif os.path.isdir(srcname):
+                copytree(srcname, dstname, symlinks, skip)
+            else:
+                shutil.copy2(srcname, dstname)
+            # XXX What about devices, sockets etc.?
+        except (IOError, os.error), why:
+            errors.append((srcname, dstname, why))
+    if errors:
+        raise shutil.Error, errors
+
+
+class TracAdmin(cmd.Cmd):
+    intro = ''
+    license = trac.__license_long__
+    doc_header = 'Trac Admin Console %(ver)s\n' \
+                 'Available Commands:\n' \
+                 % {'ver':trac.__version__ }
+    ruler = ''
+    prompt = "Trac> "
+    __env = None
+    _date_format = '%Y-%m-%d'
+    _datetime_format = '%Y-%m-%d %H:%M:%S'
+    _date_format_hint = 'YYYY-MM-DD'
+
+    def __init__(self, envdir=None):
+        cmd.Cmd.__init__(self)
+        self.interactive = False
+        if envdir:
+            self.env_set(os.path.abspath(envdir))
+        self._permsys = None
+
+    def emptyline(self):
+        pass
+
+    def onecmd(self, line):
+        """`line` may be a `str` or an `unicode` object"""
+        try:
+            if isinstance(line, str):
+                line = to_unicode(line, sys.stdin.encoding)
+            rv = cmd.Cmd.onecmd(self, line) or 0
+        except SystemExit:
+            raise
+        except Exception, e:
+            print>>sys.stderr, 'Command failed: %s' % e
+            rv = 2
+        if not self.interactive:
+            return rv
+
+    def run(self):
+        self.interactive = True
+        print 'Welcome to trac-admin %(ver)s\n'                \
+              'Interactive Trac administration console.\n'       \
+              '%(copy)s\n\n'                                    \
+              "Type:  '?' or 'help' for help on commands.\n" %  \
+              {'ver':trac.__version__,'copy':__copyright__}
+        self.cmdloop()
+
+    ##
+    ## Environment methods
+    ##
+
+    def env_set(self, envname, env=None):
+        self.envname = envname
+        self.prompt = "Trac [%s]> " % self.envname
+        if env is not None:
+            self.__env = env
+
+    def env_check(self):
+        try:
+            self.__env = Environment(self.envname)
+        except:
+            return 0
+        return 1
+
+    def env_open(self):
+        try:
+            if not self.__env:
+                self.__env = Environment(self.envname)
+            return self.__env
+        except Exception, e:
+            print 'Failed to open environment.', e
+            traceback.print_exc()
+            sys.exit(1)
+
+    def db_open(self):
+        return self.env_open().get_db_cnx()
+
+    def db_query(self, sql, cursor=None, params=None):
+        if not cursor:
+            cnx = self.db_open()
+            cursor = cnx.cursor()
+        if params:
+            cursor.execute(sql, params)
+        else:
+            cursor.execute(sql)
+        for row in cursor:
+            yield row
+
+    def db_update(self, sql, cursor=None, params=None):
+        if not cursor:
+            cnx = self.db_open()
+            cursor = cnx.cursor()
+        else:
+            cnx = None
+        if params:
+            cursor.execute(sql, params)
+        else:
+            cursor.execute(sql)
+        if cnx:
+            cnx.commit()
+
+    ##
+    ## Utility methods
+    ##
+
+    def arg_tokenize (self, argstr):
+        """`argstr` is an `unicode` string
+
+        ... but shlex is not unicode friendly.
+        """
+        return [unicode(token, 'utf-8')
+                for token in shlex.split(argstr.encode('utf-8'))] or ['']
+
+    def word_complete (self, text, words):
+        return [a for a in words if a.startswith (text)]
+
+    def print_listing(self, headers, data, sep=' ', decor=True):
+        cons_charset = sys.stdout.encoding
+        ldata = list(data)
+        if decor:
+            ldata.insert(0, headers)
+        print
+        colw = []
+        ncols = len(ldata[0]) # assumes all rows are of equal length
+        for cnum in xrange(0, ncols):
+            mw = 0
+            for cell in [unicode(d[cnum]) or '' for d in ldata]:
+                if len(cell) > mw:
+                    mw = len(cell)
+            colw.append(mw)
+        for rnum in xrange(len(ldata)):
+            for cnum in xrange(ncols):
+                if decor and rnum == 0:
+                    sp = ('%%%ds' % len(sep)) % ' '  # No separator in header
+                else:
+                    sp = sep
+                if cnum + 1 == ncols:
+                    sp = '' # No separator after last column
+                pdata = ((u'%%-%ds%s' % (colw[cnum], sp)) 
+                         % (ldata[rnum][cnum] or ''))
+                if cons_charset and isinstance(pdata, unicode):
+                    pdata = pdata.encode(cons_charset, 'replace')
+                print pdata,
+            print
+            if rnum == 0 and decor:
+                print ''.join(['-' for x in
+                               xrange(0, (1 + len(sep)) * cnum + sum(colw))])
+        print
+
+    def print_doc(cls, docs, stream=None):
+        if stream is None:
+            stream = sys.stdout
+        if not docs: return
+        for cmd, doc in docs:
+            print>>stream, cmd
+            print>>stream, '\t-- %s\n' % doc
+    print_doc = classmethod(print_doc)
+
+    def get_component_list(self):
+        rows = self.db_query("SELECT name FROM component")
+        return [row[0] for row in rows]
+
+    def get_user_list(self):
+        rows = self.db_query("SELECT DISTINCT username FROM permission")
+        return [row[0] for row in rows]
+
+    def get_wiki_list(self):
+        rows = self.db_query('SELECT DISTINCT name FROM wiki') 
+        return [row[0] for row in rows]
+
+    def get_dir_list(self, pathstr, justdirs=False):
+        dname = os.path.dirname(pathstr)
+        d = os.path.join(os.getcwd(), dname)
+        dlist = os.listdir(d)
+        if justdirs:
+            result = []
+            for entry in dlist:
+                try:
+                    if os.path.isdir(entry):
+                        result.append(entry)
+                except:
+                    pass
+        else:
+            result = dlist
+        return result
+
+    def get_enum_list(self, type):
+        rows = self.db_query("SELECT name FROM enum WHERE type=%s",
+                             params=[type])
+        return [row[0] for row in rows]
+
+    def get_milestone_list(self):
+        rows = self.db_query("SELECT name FROM milestone")
+        return [row[0] for row in rows]
+
+    def get_version_list(self):
+        rows = self.db_query("SELECT name FROM version")
+        return [row[0] for row in rows]
+
+    def _parse_date(self, t):
+        seconds = None
+        t = t.strip()
+        if t == 'now':
+            seconds = int(time.time())
+        else:
+            for format in [self._date_format, '%x %X', '%x, %X', '%X %x',
+                           '%X, %x', '%x', '%c', '%b %d, %Y']:
+                try:
+                    pt = time.strptime(t, format)
+                    seconds = int(time.mktime(pt))
+                except ValueError:
+                    continue
+                break
+        if seconds == None:
+            try:
+                seconds = int(t)
+            except ValueError:
+                pass
+        if seconds == None:
+            print>>sys.stderr, 'Unknown time format %s' % t
+        return seconds
+
+    def _format_date(self, s):
+        return time.strftime(self._date_format, time.localtime(s))
+
+    def _format_datetime(self, s):
+        return time.strftime(self._datetime_format, time.localtime(s))
+
+
+    ##
+    ## Available Commands
+    ##
+
+    ## Help
+    _help_help = [('help', 'Show documentation')]
+
+    def all_docs(cls):
+        return (cls._help_about + cls._help_help +
+                cls._help_initenv + cls._help_hotcopy +
+                cls._help_resync + cls._help_upgrade +
+                cls._help_wiki +
+#               cls._help_config + cls._help_wiki +
+                cls._help_permission + cls._help_component +
+                cls._help_ticket +
+                cls._help_ticket_type + cls._help_priority +
+                cls._help_severity +  cls._help_version +
+                cls._help_milestone)
+    all_docs = classmethod(all_docs)
+
+    def do_help(self, line=None):
+        arg = self.arg_tokenize(line)
+        if arg[0]:
+            try:
+                doc = getattr(self, "_help_" + arg[0])
+                self.print_doc(doc)
+            except AttributeError:
+                print "No documentation found for '%s'" % arg[0]
+        else:
+            print 'trac-admin - The Trac Administration Console %s' \
+                  % trac.__version__
+            if not self.interactive:
+                print
+                print "Usage: trac-admin </path/to/projenv> [command [subcommand] [option ...]]\n"
+                print "Invoking trac-admin without command starts "\
+                      "interactive mode."
+            self.print_doc(self.all_docs())
+
+    
+    ## About / Version
+    _help_about = [('about', 'Shows information about trac-admin')]
+
+    def do_about(self, line):
+        print
+        print 'Trac Admin Console %s' % trac.__version__
+        print '================================================================='
+        print self.license
+
+
+    ## Quit / EOF
+    _help_quit = [['quit', 'Exit the program']]
+    _help_exit = _help_quit
+    _help_EOF = _help_quit
+
+    def do_quit(self, line):
+        print
+        sys.exit()
+
+    do_exit = do_quit # Alias
+    do_EOF = do_quit # Alias
+
+
+    # Component
+    _help_component = [('component list', 'Show available components'),
+                       ('component add <name> <owner>', 'Add a new component'),
+                       ('component rename <name> <newname>',
+                        'Rename a component'),
+                       ('component remove <name>',
+                        'Remove/uninstall component'),
+                       ('component chown <name> <owner>',
+                        'Change component ownership')]
+
+    def complete_component(self, text, line, begidx, endidx):
+        if begidx in (16, 17):
+            comp = self.get_component_list()
+        elif begidx > 15 and line.startswith('component chown '):
+            comp = self.get_user_list()
+        else:
+            comp = ['list', 'add', 'rename', 'remove', 'chown']
+        return self.word_complete(text, comp)
+
+    def do_component(self, line):
+        arg = self.arg_tokenize(line)
+        if arg[0]  == 'list':
+            self._do_component_list()
+        elif arg[0] == 'add' and len(arg)==3:
+            name = arg[1]
+            owner = arg[2]
+            self._do_component_add(name, owner)
+        elif arg[0] == 'rename' and len(arg)==3:
+            name = arg[1]
+            newname = arg[2]
+            self._do_component_rename(name, newname)
+        elif arg[0] == 'remove'  and len(arg)==2:
+            name = arg[1]
+            self._do_component_remove(name)
+        elif arg[0] == 'chown' and len(arg)==3:
+            name = arg[1]
+            owner = arg[2]
+            self._do_component_set_owner(name, owner)
+        else:    
+            self.do_help ('component')
+
+    def _do_component_list(self):
+        data = []
+        for c in Component.select(self.env_open()):
+            data.append((c.name, c.owner))
+        self.print_listing(['Name', 'Owner'], data)
+
+    def _do_component_add(self, name, owner):
+        component = Component(self.env_open())
+        component.name = name
+        component.owner = owner
+        component.insert()
+
+    def _do_component_rename(self, name, newname):
+        component = Component(self.env_open(), name)
+        component.name = newname
+        component.update()
+
+    def _do_component_remove(self, name):
+        component = Component(self.env_open(), name)
+        component.delete()
+
+    def _do_component_set_owner(self, name, owner):
+        component = Component(self.env_open(), name)
+        component.owner = owner
+        component.update()
+
+
+    ## Permission
+    _help_permission = [('permission list [user]', 'List permission rules'),
+                        ('permission add <user> <action> [action] [...]',
+                         'Add a new permission rule'),
+                        ('permission remove <user> <action> [action] [...]',
+                         'Remove permission rule')]
+
+    def complete_permission(self, text, line, begidx, endidx):
+        argv = self.arg_tokenize(line)
+        argc = len(argv)
+        if line[-1] == ' ': # Space starts new argument
+            argc += 1
+        if argc == 2:
+            comp = ['list', 'add', 'remove']
+        elif argc >= 4:
+            comp = perm.permissions + perm.meta_permissions.keys()
+            comp.sort()
+        return self.word_complete(text, comp)
+
+    def do_permission(self, line):
+        arg = self.arg_tokenize(line)
+        if arg[0]  == 'list':
+            user = None
+            if len(arg) > 1:
+                user = arg[1]
+            self._do_permission_list(user)
+        elif arg[0] == 'add' and len(arg) >= 3:
+            user = arg[1]
+            for action in arg[2:]:
+                self._do_permission_add(user, action)
+        elif arg[0] == 'remove'  and len(arg) >= 3:
+            user = arg[1]
+            for action in arg[2:]:
+                self._do_permission_remove(user, action)
+        else:
+            self.do_help('permission')
+
+    def _do_permission_list(self, user=None):
+        if not self._permsys:
+            self._permsys = PermissionSystem(self.env_open())
+        if user:
+            rows = []
+            perms = self._permsys.get_user_permissions(user)
+            for action in perms:
+                if perms[action]:
+                    rows.append((user, action))
+        else:
+            rows = self._permsys.get_all_permissions()
+        rows.sort()
+        self.print_listing(['User', 'Action'], rows)
+        print
+        print 'Available actions:'
+        actions = self._permsys.get_actions()
+        actions.sort()
+        text = ', '.join(actions)
+        print wrap(text, initial_indent=' ', subsequent_indent=' ',
+                   linesep='\n')
+        print
+
+    def _do_permission_add(self, user, action):
+        if not self._permsys:
+            self._permsys = PermissionSystem(self.env_open())
+        if not action.islower() and not action.isupper():
+            print 'Group names must be in lower case and actions in upper case'
+            return
+        self._permsys.grant_permission(user, action)
+
+    def _do_permission_remove(self, user, action):
+        if not self._permsys:
+            self._permsys = PermissionSystem(self.env_open())
+        rows = self._permsys.get_all_permissions()
+        if action == '*':
+            for row in rows:
+                if user != '*' and user != row[0]:
+                    continue
+                self._permsys.revoke_permission(row[0], row[1])
+        else:
+            for row in rows:
+                if action != row[1]:
+                    continue
+                if user != '*' and user != row[0]:
+                    continue
+                self._permsys.revoke_permission(row[0], row[1])
+
+    ## Initenv
+    _help_initenv = [('initenv',
+                      'Create and initialize a new environment interactively'),
+                     ('initenv <projectname> <db> <repostype> <repospath> <templatepath>',
+                      'Create and initialize a new environment from arguments')]
+
+    def do_initdb(self, line):
+        self.do_initenv(line)
+
+    def get_initenv_args(self):
+        returnvals = []
+        print 'Creating a new Trac environment at %s' % self.envname
+        print
+        print 'Trac will first ask a few questions about your environment '
+        print 'in order to initalize and prepare the project database.'
+        print
+        print " Please enter the name of your project."
+        print " This name will be used in page titles and descriptions."
+        print
+        dp = 'My Project'
+        returnvals.append(raw_input('Project Name [%s]> ' % dp).strip() or dp)
+        print
+        print ' Please specify the connection string for the database to use.'
+        print ' By default, a local SQLite database is created in the environment '
+        print ' directory. It is also possible to use an already existing '
+        print ' PostgreSQL database (check the Trac documentation for the exact '
+        print ' connection string syntax).'
+        print
+        ddb = 'sqlite:db/trac.db'
+        prompt = 'Database connection string [%s]> ' % ddb
+        returnvals.append(raw_input(prompt).strip() or ddb)
+        print
+        print ' Please specify the type of version control system,'
+        print ' By default, it will be svn.'
+        print
+        print ' If you don\'t want to use Trac with version control integration, '
+        print ' choose the default here and don\'t specify a repository directory. '
+        print ' in the next question.'
+        print 
+        drpt = 'svn'
+        prompt = 'Repository type [%s]> ' % drpt
+        returnvals.append(raw_input(prompt).strip() or drpt)
+        print
+        print ' Please specify the absolute path to the version control '
+        print ' repository, or leave it blank to use Trac without a repository.'
+        print ' You can also set the repository location later.'
+        print 
+        prompt = 'Path to repository [/path/to/repos]> '
+        returnvals.append(raw_input(prompt).strip())
+        print
+        print ' Please enter location of Trac page templates.'
+        print ' Default is the location of the site-wide templates installed with Trac.'
+        print
+        dt = default_dir('templates')
+        prompt = 'Templates directory [%s]> ' % dt
+        returnvals.append(raw_input(prompt).strip() or dt)
+        print
+        return returnvals
+
+    def do_initenv(self, line):
+        if self.env_check():
+            print "Initenv for '%s' failed." % self.envname
+            print "Does an environment already exist?"
+            return 2
+
+        if os.path.exists(self.envname) and os.listdir(self.envname):
+            print "Initenv for '%s' failed." % self.envname
+            print "Directory exists and is not empty."
+            return 2
+
+        arg = self.arg_tokenize(line)
+        project_name = None
+        db_str = None
+        repository_dir = None
+        templates_dir = None
+        if len(arg) == 1 and not arg[0]:
+            returnvals = self.get_initenv_args()
+            project_name, db_str, repository_type, repository_dir, \
+                          templates_dir = returnvals
+        elif len(arg) != 5:
+            print 'Wrong number of arguments to initenv: %d' % len(arg)
+            return 2
+        else:
+            project_name, db_str, repository_type, repository_dir, \
+                          templates_dir = arg[:5]
+
+        if not os.access(os.path.join(templates_dir, 'header.cs'), os.F_OK):
+            print templates_dir, "doesn't look like a Trac templates directory"
+            return 2
+
+        try:
+            print 'Creating and Initializing Project'
+            options = [
+                ('trac', 'database', db_str),
+                ('trac', 'repository_type', repository_type),
+                ('trac', 'repository_dir', repository_dir),
+                ('trac', 'templates_dir', templates_dir),
+                ('project', 'name', project_name),
+            ]
+            try:
+                self.__env = Environment(self.envname, create=True,
+                                         options=options)
+            except Exception, e:
+                print 'Failed to create environment.', e
+                traceback.print_exc()
+                sys.exit(1)
+
+            # Add a few default wiki pages
+            print ' Installing default wiki pages'
+            cnx = self.__env.get_db_cnx()
+            cursor = cnx.cursor()
+            self._do_wiki_load(default_dir('wiki'), cursor)
+            cnx.commit()
+
+            if repository_dir:
+                try:
+                    repos = self.__env.get_repository()
+                    if repos:
+                        print ' Indexing repository'
+                        repos.sync()
+                except TracError, e:
+                    print>>sys.stderr, "\nWarning:\n"
+                    if repository_type == "svn":
+                        print>>sys.stderr, "You should install the SVN bindings"
+                    else:
+                        print>>sys.stderr, "Repository type %s not supported" \
+                                           % repository_type
+        except Exception, e:
+            print 'Failed to initialize environment.', e
+            traceback.print_exc()
+            return 2
+
+        print """
+---------------------------------------------------------------------
+Project environment for '%(project_name)s' created.
+
+You may now configure the environment by editing the file:
+
+  %(config_path)s
+
+If you'd like to take this new project environment for a test drive,
+try running the Trac standalone web server `tracd`:
+
+  tracd --port 8000 %(project_path)s
+
+Then point your browser to http://localhost:8000/%(project_dir)s.
+There you can also browse the documentation for your installed
+version of Trac, including information on further setup (such as
+deploying Trac to a real web server).
+
+The latest documentation can also always be found on the project
+website:
+
+  http://projects.edgewall.com/trac/
+
+Congratulations!
+""" % dict(project_name=project_name, project_path=self.envname,
+           project_dir=os.path.basename(self.envname),
+           config_path=os.path.join(self.envname, 'conf', 'trac.ini'))
+
+    _help_resync = [('resync', 'Re-synchronize trac with the repository')]
+
+    ## Resync
+    def do_resync(self, line):
+        print 'Resyncing repository history...'
+        cnx = self.db_open()
+        cursor = cnx.cursor()
+        cursor.execute("DELETE FROM revision")
+        cursor.execute("DELETE FROM node_change")
+        repos = self.__env.get_repository()
+        cursor.execute("DELETE FROM system WHERE name='repository_dir'")
+        cursor.execute("INSERT INTO system (name,value) "
+                       "VALUES ('repository_dir',%s)", (repos.name,))
+        repos.sync()
+        print 'Done.'
+
+    ## Wiki
+    _help_wiki = [('wiki list', 'List wiki pages'),
+                  ('wiki remove <name>', 'Remove wiki page'),
+                  ('wiki export <page> [file]',
+                   'Export wiki page to file or stdout'),
+                  ('wiki import <page> [file]',
+                   'Import wiki page from file or stdin'),
+                  ('wiki dump <directory>',
+                   'Export all wiki pages to files named by title'),
+                  ('wiki load <directory>',
+                   'Import all wiki pages from directory'),
+                  ('wiki upgrade',
+                   'Upgrade default wiki pages to current version')]
+
+    def complete_wiki(self, text, line, begidx, endidx):
+        argv = self.arg_tokenize(line)
+        argc = len(argv)
+        if line[-1] == ' ': # Space starts new argument
+            argc += 1
+        if argc == 2:
+            comp = ['list', 'remove', 'import', 'export', 'dump', 'load',
+                    'upgrade']
+        else:
+            if argv[1] in ('dump', 'load'):
+                comp = self.get_dir_list(argv[-1], 1)
+            elif argv[1] == 'remove':
+                comp = self.get_wiki_list()
+            elif argv[1] in ('export', 'import'):
+                if argc == 3:
+                    comp = self.get_wiki_list()
+                elif argc == 4:
+                    comp = self.get_dir_list(argv[-1])
+        return self.word_complete(text, comp)
+
+    def do_wiki(self, line):
+        arg = self.arg_tokenize(line)
+        if arg[0]  == 'list':
+            self._do_wiki_list()
+        elif arg[0] == 'remove'  and len(arg)==2:
+            name = arg[1]
+            self._do_wiki_remove(name)
+        elif arg[0] == 'import' and len(arg) == 3:
+            title = arg[1]
+            file = arg[2]
+            self._do_wiki_import(file, title)
+        elif arg[0] == 'export'  and len(arg) in [2,3]:
+            page = arg[1]
+            file = (len(arg) == 3 and arg[2]) or None
+            self._do_wiki_export(page, file)
+        elif arg[0] == 'dump' and len(arg) in [1,2]:
+            dir = (len(arg) == 2 and arg[1]) or ''
+            self._do_wiki_dump(dir)
+        elif arg[0] == 'load' and len(arg) in [1,2]:
+            dir = (len(arg) == 2 and arg[1]) or ''
+            self._do_wiki_load(dir)
+        elif arg[0] == 'upgrade' and len(arg) == 1:
+            self._do_wiki_load(default_dir('wiki'),
+                               ignore=['WikiStart', 'checkwiki.py'],
+                               create_only=['InterMapTxt'])
+        else:    
+            self.do_help ('wiki')
+
+    def _do_wiki_list(self):
+        rows = self.db_query("SELECT name, max(version), max(time) "
+                             "FROM wiki GROUP BY name ORDER BY name")
+        self.print_listing(['Title', 'Edits', 'Modified'],
+                           [(r[0], r[1], self._format_datetime(r[2])) for r in rows])
+
+    def _do_wiki_remove(self, name):
+        page = WikiPage(self.env_open(), name)
+        page.delete()
+
+    def _do_wiki_import(self, filename, title, cursor=None,
+                        create_only=[]):
+        if not os.path.isfile(filename):
+            raise Exception, '%s is not a file' % filename
+
+        f = open(filename,'r')
+        data = to_unicode(f.read(), 'utf-8')
+
+        # Make sure we don't insert the exact same page twice
+        rows = self.db_query("SELECT text FROM wiki WHERE name=%s "
+                             "ORDER BY version DESC LIMIT 1", cursor,
+                             params=(title,))
+        old = list(rows)
+        if old and title in create_only:
+            print '  %s already exists.' % title
+            return
+        if old and data == old[0][0]:
+            print '  %s already up to date.' % title
+            return
+        f.close()
+
+        self.db_update("INSERT INTO wiki(version,name,time,author,ipnr,text) "
+                       " SELECT 1+COALESCE(max(version),0),%s,%s,"
+                       " 'trac','127.0.0.1',%s FROM wiki "
+                       " WHERE name=%s",
+                       cursor, (title, int(time.time()), data, title))
+
+    def _do_wiki_export(self, page, filename=''):
+        data = self.db_query("SELECT text FROM wiki WHERE name=%s "
+                             "ORDER BY version DESC LIMIT 1", params=[page])
+        text = data.next()[0]
+        if not filename:
+            print text
+        else:
+            if os.path.isfile(filename):
+                raise Exception("File '%s' exists" % filename)
+            f = open(filename,'w')
+            f.write(text.encode('utf-8'))
+            f.close()
+
+    def _do_wiki_dump(self, dir):
+        pages = self.get_wiki_list()
+        for p in pages:
+            dst = os.path.join(dir, urllib.quote(p, ''))
+            print " %s => %s" % (p, dst)
+            self._do_wiki_export(p, dst)
+
+    def _do_wiki_load(self, dir, cursor=None, ignore=[], create_only=[]):
+        for page in os.listdir(dir):
+            if page in ignore:
+                continue
+            filename = os.path.join(dir, page)
+            page = urllib.unquote(page)
+            if os.path.isfile(filename):
+                print " %s => %s" % (filename, page)
+                self._do_wiki_import(filename, page, cursor, create_only)
+
+    ## Ticket
+    _help_ticket = [('ticket remove <number>', 'Remove ticket')]
+
+    def complete_ticket(self, text, line, begidx, endidx):
+        argv = self.arg_tokenize(line)
+        argc = len(argv)
+        if line[-1] == ' ': # Space starts new argument
+            argc += 1
+        comp = []
+        if argc == 2:
+            comp = ['remove']
+        return self.word_complete(text, comp)
+
+    def do_ticket(self, line):
+        arg = self.arg_tokenize(line)
+        if arg[0] == 'remove'  and len(arg)==2:
+            try:
+                number = int(arg[1])
+            except ValueError:
+                print>>sys.stderr, "<number> must be a number"
+                return
+            self._do_ticket_remove(number)
+        else:    
+            self.do_help ('ticket')
+
+    def _do_ticket_remove(self, number):
+        ticket = Ticket(self.env_open(), number)
+        ticket.delete()
+        print "Ticket %d and all associated data removed." % number
+
+
+    ## (Ticket) Type
+    _help_ticket_type = [('ticket_type list', 'Show possible ticket types'),
+                         ('ticket_type add <value>', 'Add a ticket type'),
+                         ('ticket_type change <value> <newvalue>',
+                          'Change a ticket type'),
+                         ('ticket_type remove <value>', 'Remove a ticket type'),
+                         ('ticket_type order <value> up|down',
+                          'Move a ticket type up or down in the list')]
+
+    def complete_ticket_type (self, text, line, begidx, endidx):
+        if begidx == 16:
+            comp = self.get_enum_list ('ticket_type')
+        elif begidx < 15:
+            comp = ['list', 'add', 'change', 'remove', 'order']
+        return self.word_complete(text, comp)
+ 
+    def do_ticket_type(self, line):
+        self._do_enum('ticket_type', line)
+ 
+    ## (Ticket) Priority
+    _help_priority = [('priority list', 'Show possible ticket priorities'),
+                       ('priority add <value>', 'Add a priority value option'),
+                       ('priority change <value> <newvalue>',
+                        'Change a priority value'),
+                       ('priority remove <value>', 'Remove priority value'),
+                       ('priority order <value> up|down',
+                        'Move a priority value up or down in the list')]
+
+    def complete_priority (self, text, line, begidx, endidx):
+        if begidx == 16:
+            comp = self.get_enum_list ('priority')
+        elif begidx < 15:
+            comp = ['list', 'add', 'change', 'remove', 'order']
+        return self.word_complete(text, comp)
+
+    def do_priority(self, line):
+        self._do_enum('priority', line)
+
+    ## (Ticket) Severity
+    _help_severity = [('severity list', 'Show possible ticket severities'),
+                      ('severity add <value>', 'Add a severity value option'),
+                      ('severity change <value> <newvalue>',
+                       'Change a severity value'),
+                      ('severity remove <value>', 'Remove severity value'),
+                      ('severity order <value> up|down',
+                       'Move a severity value up or down in the list')]
+
+    def complete_severity (self, text, line, begidx, endidx):
+        if begidx == 16:
+            comp = self.get_enum_list ('severity')
+        elif begidx < 15:
+            comp = ['list', 'add', 'change', 'remove', 'order']
+        return self.word_complete(text, comp)
+
+    def do_severity(self, line):
+        self._do_enum('severity', line)
+
+    # Type, priority, severity share the same datastructure and methods:
+
+    _enum_map = {'ticket_type': Type, 'priority': Priority,
+                 'severity': Severity}
+
+    def _do_enum(self, type, line):
+        arg = self.arg_tokenize(line)
+        if arg[0]  == 'list':
+            self._do_enum_list(type)
+        elif arg[0] == 'add' and len(arg) == 2:
+            name = arg[1]
+            self._do_enum_add(type, name)
+        elif arg[0] == 'change' and len(arg) == 3:
+            name = arg[1]
+            newname = arg[2]
+            self._do_enum_change(type, name, newname)
+        elif arg[0] == 'remove' and len(arg) == 2:
+            name = arg[1]
+            self._do_enum_remove(type, name)
+        elif arg[0] == 'order' and len(arg) == 3 and arg[2] in ('up', 'down'):
+            name = arg[1]
+            if arg[2] == 'up':
+                direction = -1
+            else:
+                direction = 1
+            self._do_enum_order(type, name, direction)
+        else:    
+            self.do_help(type)
+
+    def _do_enum_list(self, type):
+        enum_cls = self._enum_map[type]
+        self.print_listing(['Possible Values'],
+                           [(e.name,) for e in enum_cls.select(self.env_open())])
+
+    def _do_enum_add(self, type, name):
+        cnx = self.db_open()
+        sql = ("INSERT INTO enum(value,type,name) "
+               " SELECT 1+COALESCE(max(%(cast)s),0),'%(type)s','%(name)s'"
+               "   FROM enum WHERE type='%(type)s'" 
+               % {'type':type, 'name':name, 'cast': cnx.cast('value', 'int')})
+        cursor = cnx.cursor()
+        self.db_update(sql, cursor)
+        cnx.commit()
+
+    def _do_enum_change(self, type, name, newname):
+        enum_cls = self._enum_map[type]
+        enum = enum_cls(self.env_open(), name)
+        enum.name = newname
+        enum.update()
+
+    def _do_enum_remove(self, type, name):
+        enum_cls = self._enum_map[type]
+        enum = enum_cls(self.env_open(), name)
+        enum.delete()
+
+    def _do_enum_order(self, type, name, direction):
+        env = self.env_open()
+        enum_cls = self._enum_map[type]
+        enum1 = enum_cls(env, name)
+        enum1.value = int(float(enum1.value) + direction)
+        for enum2 in enum_cls.select(env):
+            if int(float(enum2.value)) == enum1.value:
+                enum2.value = int(float(enum2.value) - direction)
+                break
+        else:
+            return
+        enum1.update()
+        enum2.update()
+
+    ## Milestone
+
+    _help_milestone = [('milestone list', 'Show milestones'),
+                       ('milestone add <name> [due]', 'Add milestone'),
+                       ('milestone rename <name> <newname>',
+                        'Rename milestone'),
+                       ('milestone due <name> <due>',
+                        'Set milestone due date (Format: "%s" or "now")'
+                        % _date_format_hint),
+                       ('milestone completed <name> <completed>',
+                        'Set milestone completed date (Format: "%s" or "now")'
+                        % _date_format_hint),
+                       ('milestone remove <name>', 'Remove milestone')]
+
+    def complete_milestone (self, text, line, begidx, endidx):
+        if begidx in (15, 17):
+            comp = self.get_milestone_list()
+        elif begidx < 15:
+            comp = ['list', 'add', 'rename', 'time', 'remove']
+        return self.word_complete(text, comp)
+
+    def do_milestone(self, line):
+        arg = self.arg_tokenize(line)
+        if arg[0]  == 'list':
+            self._do_milestone_list()
+        elif arg[0] == 'add' and len(arg) in [2,3]:
+            self._do_milestone_add(arg[1])
+            if len(arg) == 3:
+                self._do_milestone_set_due(arg[1], arg[2])
+        elif arg[0] == 'rename' and len(arg) == 3:
+            self._do_milestone_rename(arg[1], arg[2])
+        elif arg[0] == 'remove' and len(arg) == 2:
+            self._do_milestone_remove(arg[1])
+        elif arg[0] == 'due' and len(arg) == 3:
+            self._do_milestone_set_due(arg[1], arg[2])
+        elif arg[0] == 'completed' and len(arg) == 3:
+            self._do_milestone_set_completed(arg[1], arg[2])
+        else:
+            self.do_help('milestone')
+
+    def _do_milestone_list(self):
+        data = []
+        for m in Milestone.select(self.env_open()):
+            data.append((m.name, m.due and self._format_date(m.due),
+                         m.completed and self._format_datetime(m.completed)))
+
+        self.print_listing(['Name', 'Due', 'Completed'], data)
+
+    def _do_milestone_rename(self, name, newname):
+        milestone = Milestone(self.env_open(), name)
+        milestone.name = newname
+        milestone.update()
+
+    def _do_milestone_add(self, name):
+        milestone = Milestone(self.env_open())
+        milestone.name = name
+        milestone.insert()
+
+    def _do_milestone_remove(self, name):
+        milestone = Milestone(self.env_open(), name)
+        milestone.delete(author=getpass.getuser())
+
+    def _do_milestone_set_due(self, name, t):
+        milestone = Milestone(self.env_open(), name)
+        milestone.due = self._parse_date(t)
+        milestone.update()
+
+    def _do_milestone_set_completed(self, name, t):
+        milestone = Milestone(self.env_open(), name)
+        milestone.completed = self._parse_date(t)
+        milestone.update()
+
+    ## Version
+    _help_version = [('version list', 'Show versions'),
+                       ('version add <name> [time]', 'Add version'),
+                       ('version rename <name> <newname>',
+                        'Rename version'),
+                       ('version time <name> <time>',
+                        'Set version date (Format: "%s" or "now")'
+                        % _date_format_hint),
+                       ('version remove <name>', 'Remove version')]
+
+    def complete_version (self, text, line, begidx, endidx):
+        if begidx in (13, 15):
+            comp = self.get_version_list()
+        elif begidx < 13:
+            comp = ['list', 'add', 'rename', 'time', 'remove']
+        return self.word_complete(text, comp)
+
+    def do_version(self, line):
+        arg = self.arg_tokenize(line)
+        if arg[0]  == 'list':
+            self._do_version_list()
+        elif arg[0] == 'add' and len(arg) in [2,3]:
+            self._do_version_add(arg[1])
+            if len(arg) == 3:
+                self._do_version_time(arg[1], arg[2])
+        elif arg[0] == 'rename' and len(arg) == 3:
+            self._do_version_rename(arg[1], arg[2])
+        elif arg[0] == 'time' and len(arg) == 3:
+            self._do_version_time(arg[1], arg[2])
+        elif arg[0] == 'remove' and len(arg) == 2:
+            self._do_version_remove(arg[1])
+        else:
+            self.do_help('version')
+
+    def _do_version_list(self):
+        data = []
+        for v in Version.select(self.env_open()):
+            data.append((v.name, v.time and self._format_date(v.time)))
+        self.print_listing(['Name', 'Time'], data)
+
+    def _do_version_rename(self, name, newname):
+        version = Version(self.env_open(), name)
+        version.name = newname
+        version.update()
+
+    def _do_version_add(self, name):
+        version = Version(self.env_open())
+        version.name = name
+        version.insert()
+
+    def _do_version_remove(self, name):
+        version = Version(self.env_open(), name)
+        version.delete()
+
+    def _do_version_time(self, name, t):
+        version = Version(self.env_open(), name)
+        version.time = self._parse_date(t)
+        version.update()
+
+    _help_upgrade = [('upgrade', 'Upgrade database to current version')]
+    def do_upgrade(self, line):
+        arg = self.arg_tokenize(line)
+        do_backup = True
+        if arg[0] in ['-b', '--no-backup']:
+            do_backup = False
+        self.db_open()
+
+        if not self.__env.needs_upgrade():
+            print "Database is up to date, no upgrade necessary."
+            return
+
+        self.__env.upgrade(backup=do_backup)
+        print 'Upgrade done.'
+
+    _help_hotcopy = [('hotcopy <backupdir>',
+                      'Make a hot backup copy of an environment')]
+    def do_hotcopy(self, line):
+        arg = self.arg_tokenize(line)
+        if arg[0]:
+            dest = arg[0]
+        else:
+            self.do_help('hotcopy')
+            return
+
+        # Bogus statement to lock the database while copying files
+        cnx = self.db_open()
+        cursor = cnx.cursor()
+        cursor.execute("UPDATE system SET name=NULL WHERE name IS NULL")
+
+        try:
+            print 'Hotcopying %s to %s ...' % (self.__env.path, dest),
+            db_str = self.__env.config.get('trac', 'database')
+            prefix, db_path = db_str.split(':', 1)
+            if prefix == 'sqlite':
+                # don't copy the journal (also, this would fail on Windows)
+                db_path = os.path.normpath(db_path)
+                skip = ['%s-journal' % os.path.join(self.__env.path, db_path)]
+            else:
+                skip = []
+            copytree(self.__env.path, dest, symlinks=1, skip=skip)
+        finally:
+            # Unlock database
+            cnx.rollback()
+
+        print 'Hotcopy done.'
+
+
+class TracAdminHelpMacro(WikiMacroBase):
+    """Displays help for trac-admin commands.
+
+    Examples:
+    {{{
+    [[TracAdminHelp]]               # all commands
+    [[TracAdminHelp(wiki)]]         # all wiki commands
+    [[TracAdminHelp(wiki export)]]  # the "wiki export" command
+    [[TracAdminHelp(upgrade)]]      # the upgrade command
+    }}}
+    """
+
+    def render_macro(self, req, name, content):
+        if content:
+            try:
+                arg = content.split(' ', 1)[0]
+                doc = getattr(TracAdmin, '_help_' + arg)
+            except AttributeError:
+                raise TracError('Unknown trac-admin command "%s"' % content)
+            if arg != content:
+                for cmd, help in doc:
+                    if cmd.startswith(content):
+                        doc = [(cmd, help)]
+                        break
+        else:
+            doc = TracAdmin.all_docs()
+        buf = StringIO.StringIO()
+        TracAdmin.print_doc(doc, buf)
+        return html.PRE(buf.getvalue(), class_='wiki')
+
+
+def run(args):
+    """Main entry point."""
+    admin = TracAdmin()
+    if len(args) > 0:
+        if args[0] in ('-h', '--help', 'help'):
+            return admin.onecmd("help")
+        elif args[0] in ('-v','--version','about'):
+            return admin.onecmd("about")
+        else:
+            admin.env_set(os.path.abspath(args[0]))
+            if len(args) > 1:
+                s_args = ' '.join(["'%s'" % c for c in args[2:]])
+                command = args[1] + ' ' +s_args
+                return admin.onecmd(command)
+            else:
+                while True:
+                    admin.run()
+    else:
+        return admin.onecmd("help")
+
+
+if __name__ == '__main__':
+    run(sys.argv[1:])
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/scripts/tests/__init__.py
@@ -0,0 +1,12 @@
+import unittest
+
+from trac.scripts.tests import admin
+
+def suite():
+
+    suite = unittest.TestSuite()
+    suite.addTest(admin.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/scripts/tests/admin-tests.txt
@@ -0,0 +1,644 @@
+===== test_help_ok =====
+trac-admin - The Trac Administration Console 0.10dev
+
+Usage: trac-admin </path/to/projenv> [command [subcommand] [option ...]]
+
+Invoking trac-admin without command starts interactive mode.
+about
+	-- Shows information about trac-admin
+
+help
+	-- Show documentation
+
+initenv
+	-- Create and initialize a new environment interactively
+
+initenv <projectname> <db> <repostype> <repospath> <templatepath>
+	-- Create and initialize a new environment from arguments
+
+hotcopy <backupdir>
+	-- Make a hot backup copy of an environment
+
+resync
+	-- Re-synchronize trac with the repository
+
+upgrade
+	-- Upgrade database to current version
+
+wiki list
+	-- List wiki pages
+
+wiki remove <name>
+	-- Remove wiki page
+
+wiki export <page> [file]
+	-- Export wiki page to file or stdout
+
+wiki import <page> [file]
+	-- Import wiki page from file or stdin
+
+wiki dump <directory>
+	-- Export all wiki pages to files named by title
+
+wiki load <directory>
+	-- Import all wiki pages from directory
+
+wiki upgrade
+	-- Upgrade default wiki pages to current version
+
+permission list [user]
+	-- List permission rules
+
+permission add <user> <action> [action] [...]
+	-- Add a new permission rule
+
+permission remove <user> <action> [action] [...]
+	-- Remove permission rule
+
+component list
+	-- Show available components
+
+component add <name> <owner>
+	-- Add a new component
+
+component rename <name> <newname>
+	-- Rename a component
+
+component remove <name>
+	-- Remove/uninstall component
+
+component chown <name> <owner>
+	-- Change component ownership
+
+ticket remove <number>
+	-- Remove ticket
+
+ticket_type list
+	-- Show possible ticket types
+
+ticket_type add <value>
+	-- Add a ticket type
+
+ticket_type change <value> <newvalue>
+	-- Change a ticket type
+
+ticket_type remove <value>
+	-- Remove a ticket type
+
+ticket_type order <value> up|down
+	-- Move a ticket type up or down in the list
+
+priority list
+	-- Show possible ticket priorities
+
+priority add <value>
+	-- Add a priority value option
+
+priority change <value> <newvalue>
+	-- Change a priority value
+
+priority remove <value>
+	-- Remove priority value
+
+priority order <value> up|down
+	-- Move a priority value up or down in the list
+
+severity list
+	-- Show possible ticket severities
+
+severity add <value>
+	-- Add a severity value option
+
+severity change <value> <newvalue>
+	-- Change a severity value
+
+severity remove <value>
+	-- Remove severity value
+
+severity order <value> up|down
+	-- Move a severity value up or down in the list
+
+version list
+	-- Show versions
+
+version add <name> [time]
+	-- Add version
+
+version rename <name> <newname>
+	-- Rename version
+
+version time <name> <time>
+	-- Set version date (Format: "YYYY-MM-DD" or "now")
+
+version remove <name>
+	-- Remove version
+
+milestone list
+	-- Show milestones
+
+milestone add <name> [due]
+	-- Add milestone
+
+milestone rename <name> <newname>
+	-- Rename milestone
+
+milestone due <name> <due>
+	-- Set milestone due date (Format: "YYYY-MM-DD" or "now")
+
+milestone completed <name> <completed>
+	-- Set milestone completed date (Format: "YYYY-MM-DD" or "now")
+
+milestone remove <name>
+	-- Remove milestone
+
+===== test_permission_list_ok =====
+
+User       Action         
+--------------------------
+anonymous  BROWSER_VIEW   
+anonymous  CHANGESET_VIEW 
+anonymous  FILE_VIEW      
+anonymous  LOG_VIEW       
+anonymous  MILESTONE_VIEW 
+anonymous  REPORT_SQL_VIEW
+anonymous  REPORT_VIEW    
+anonymous  ROADMAP_VIEW   
+anonymous  SEARCH_VIEW    
+anonymous  TICKET_CREATE  
+anonymous  TICKET_MODIFY  
+anonymous  TICKET_VIEW    
+anonymous  TIMELINE_VIEW  
+anonymous  WIKI_CREATE    
+anonymous  WIKI_MODIFY    
+anonymous  WIKI_VIEW      
+
+
+Available actions:
+ BROWSER_VIEW, CHANGESET_VIEW, CONFIG_VIEW, FILE_VIEW, LOG_VIEW,
+ MILESTONE_ADMIN, MILESTONE_CREATE, MILESTONE_DELETE, MILESTONE_MODIFY,
+ MILESTONE_VIEW, REPORT_ADMIN, REPORT_CREATE, REPORT_DELETE, REPORT_MODIFY,
+ REPORT_SQL_VIEW, REPORT_VIEW, ROADMAP_ADMIN, ROADMAP_VIEW, SEARCH_VIEW,
+ TICKET_ADMIN, TICKET_APPEND, TICKET_CHGPROP, TICKET_CREATE, TICKET_MODIFY,
+ TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, WIKI_ADMIN, WIKI_CREATE,
+ WIKI_DELETE, WIKI_MODIFY, WIKI_VIEW
+
+===== test_permission_add_one_action_ok =====
+
+User       Action         
+--------------------------
+anonymous  BROWSER_VIEW   
+anonymous  CHANGESET_VIEW 
+anonymous  FILE_VIEW      
+anonymous  LOG_VIEW       
+anonymous  MILESTONE_VIEW 
+anonymous  REPORT_SQL_VIEW
+anonymous  REPORT_VIEW    
+anonymous  ROADMAP_VIEW   
+anonymous  SEARCH_VIEW    
+anonymous  TICKET_CREATE  
+anonymous  TICKET_MODIFY  
+anonymous  TICKET_VIEW    
+anonymous  TIMELINE_VIEW  
+anonymous  WIKI_CREATE    
+anonymous  WIKI_MODIFY    
+anonymous  WIKI_VIEW      
+test_user  WIKI_VIEW      
+
+
+Available actions:
+ BROWSER_VIEW, CHANGESET_VIEW, CONFIG_VIEW, FILE_VIEW, LOG_VIEW,
+ MILESTONE_ADMIN, MILESTONE_CREATE, MILESTONE_DELETE, MILESTONE_MODIFY,
+ MILESTONE_VIEW, REPORT_ADMIN, REPORT_CREATE, REPORT_DELETE, REPORT_MODIFY,
+ REPORT_SQL_VIEW, REPORT_VIEW, ROADMAP_ADMIN, ROADMAP_VIEW, SEARCH_VIEW,
+ TICKET_ADMIN, TICKET_APPEND, TICKET_CHGPROP, TICKET_CREATE, TICKET_MODIFY,
+ TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, WIKI_ADMIN, WIKI_CREATE,
+ WIKI_DELETE, WIKI_MODIFY, WIKI_VIEW
+
+===== test_permission_add_multiple_actions_ok =====
+
+User       Action         
+--------------------------
+anonymous  BROWSER_VIEW   
+anonymous  CHANGESET_VIEW 
+anonymous  FILE_VIEW      
+anonymous  LOG_VIEW       
+anonymous  MILESTONE_VIEW 
+anonymous  REPORT_SQL_VIEW
+anonymous  REPORT_VIEW    
+anonymous  ROADMAP_VIEW   
+anonymous  SEARCH_VIEW    
+anonymous  TICKET_CREATE  
+anonymous  TICKET_MODIFY  
+anonymous  TICKET_VIEW    
+anonymous  TIMELINE_VIEW  
+anonymous  WIKI_CREATE    
+anonymous  WIKI_MODIFY    
+anonymous  WIKI_VIEW      
+test_user  FILE_VIEW      
+test_user  LOG_VIEW       
+
+
+Available actions:
+ BROWSER_VIEW, CHANGESET_VIEW, CONFIG_VIEW, FILE_VIEW, LOG_VIEW,
+ MILESTONE_ADMIN, MILESTONE_CREATE, MILESTONE_DELETE, MILESTONE_MODIFY,
+ MILESTONE_VIEW, REPORT_ADMIN, REPORT_CREATE, REPORT_DELETE, REPORT_MODIFY,
+ REPORT_SQL_VIEW, REPORT_VIEW, ROADMAP_ADMIN, ROADMAP_VIEW, SEARCH_VIEW,
+ TICKET_ADMIN, TICKET_APPEND, TICKET_CHGPROP, TICKET_CREATE, TICKET_MODIFY,
+ TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, WIKI_ADMIN, WIKI_CREATE,
+ WIKI_DELETE, WIKI_MODIFY, WIKI_VIEW
+
+===== test_permission_remove_one_action_ok =====
+
+User       Action         
+--------------------------
+anonymous  BROWSER_VIEW   
+anonymous  CHANGESET_VIEW 
+anonymous  FILE_VIEW      
+anonymous  LOG_VIEW       
+anonymous  MILESTONE_VIEW 
+anonymous  REPORT_SQL_VIEW
+anonymous  REPORT_VIEW    
+anonymous  ROADMAP_VIEW   
+anonymous  SEARCH_VIEW    
+anonymous  TICKET_CREATE  
+anonymous  TICKET_VIEW    
+anonymous  TIMELINE_VIEW  
+anonymous  WIKI_CREATE    
+anonymous  WIKI_MODIFY    
+anonymous  WIKI_VIEW      
+
+
+Available actions:
+ BROWSER_VIEW, CHANGESET_VIEW, CONFIG_VIEW, FILE_VIEW, LOG_VIEW,
+ MILESTONE_ADMIN, MILESTONE_CREATE, MILESTONE_DELETE, MILESTONE_MODIFY,
+ MILESTONE_VIEW, REPORT_ADMIN, REPORT_CREATE, REPORT_DELETE, REPORT_MODIFY,
+ REPORT_SQL_VIEW, REPORT_VIEW, ROADMAP_ADMIN, ROADMAP_VIEW, SEARCH_VIEW,
+ TICKET_ADMIN, TICKET_APPEND, TICKET_CHGPROP, TICKET_CREATE, TICKET_MODIFY,
+ TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, WIKI_ADMIN, WIKI_CREATE,
+ WIKI_DELETE, WIKI_MODIFY, WIKI_VIEW
+
+===== test_permission_remove_multiple_actions_ok =====
+
+User       Action         
+--------------------------
+anonymous  BROWSER_VIEW   
+anonymous  CHANGESET_VIEW 
+anonymous  FILE_VIEW      
+anonymous  LOG_VIEW       
+anonymous  MILESTONE_VIEW 
+anonymous  REPORT_SQL_VIEW
+anonymous  REPORT_VIEW    
+anonymous  ROADMAP_VIEW   
+anonymous  SEARCH_VIEW    
+anonymous  TICKET_CREATE  
+anonymous  TICKET_MODIFY  
+anonymous  TICKET_VIEW    
+anonymous  TIMELINE_VIEW  
+anonymous  WIKI_VIEW      
+
+
+Available actions:
+ BROWSER_VIEW, CHANGESET_VIEW, CONFIG_VIEW, FILE_VIEW, LOG_VIEW,
+ MILESTONE_ADMIN, MILESTONE_CREATE, MILESTONE_DELETE, MILESTONE_MODIFY,
+ MILESTONE_VIEW, REPORT_ADMIN, REPORT_CREATE, REPORT_DELETE, REPORT_MODIFY,
+ REPORT_SQL_VIEW, REPORT_VIEW, ROADMAP_ADMIN, ROADMAP_VIEW, SEARCH_VIEW,
+ TICKET_ADMIN, TICKET_APPEND, TICKET_CHGPROP, TICKET_CREATE, TICKET_MODIFY,
+ TICKET_VIEW, TIMELINE_VIEW, TRAC_ADMIN, WIKI_ADMIN, WIKI_CREATE,
+ WIKI_DELETE, WIKI_MODIFY, WIKI_VIEW
+
+===== test_component_list_ok =====
+
+Name        Owner   
+--------------------
+component1  somebody
+component2  somebody
+
+===== test_component_add_ok =====
+
+Name           Owner   
+-----------------------
+component1     somebody
+component2     somebody
+new_component  new_user
+
+===== test_component_add_error_already_exists =====
+Command failed: column name is not unique
+===== test_component_rename_ok =====
+
+Name          Owner   
+----------------------
+changed_name  somebody
+component2    somebody
+
+===== test_component_rename_error_bad_component =====
+Command failed: Component bad_component does not exist.
+===== test_component_rename_error_bad_new_name =====
+Command failed: column name is not unique
+===== test_component_chown_ok =====
+
+Name        Owner        
+-------------------------
+component1  somebody     
+component2  changed_owner
+
+===== test_component_chown_error_bad_component =====
+Command failed: Component bad_component does not exist.
+===== test_component_remove_ok =====
+
+Name        Owner   
+--------------------
+component2  somebody
+
+===== test_component_remove_error_bad_component =====
+Command failed: Component bad_component does not exist.
+===== test_ticket_type_list_ok =====
+
+Possible Values
+---------------
+defect
+enhancement
+task
+
+===== test_ticket_type_add_ok =====
+
+Possible Values
+---------------
+defect
+enhancement
+task
+new_type
+
+===== test_ticket_type_add_error_already_exists =====
+Command failed: columns type, name are not unique
+===== test_ticket_type_change_ok =====
+
+Possible Values
+---------------
+bug
+enhancement
+task
+
+===== test_ticket_type_change_error_bad_type =====
+Command failed: ticket_type bad_type does not exist.
+===== test_ticket_type_change_error_bad_new_name =====
+Command failed: columns type, name are not unique
+===== test_ticket_type_remove_ok =====
+
+Possible Values
+---------------
+defect
+enhancement
+
+===== test_ticket_type_remove_error_bad_type =====
+Command failed: ticket_type bad_type does not exist.
+===== test_ticket_type_order_down_ok =====
+
+Possible Values
+---------------
+enhancement
+defect
+task
+
+===== test_ticket_type_order_up_ok =====
+
+Possible Values
+---------------
+enhancement
+defect
+task
+
+===== test_ticket_type_order_error_bad_type =====
+Command failed: ticket_type bad_type does not exist.
+===== test_priority_list_ok =====
+
+Possible Values
+---------------
+blocker        
+critical       
+major          
+minor          
+trivial    
+
+===== test_priority_add_ok =====
+
+Possible Values
+---------------
+blocker        
+critical       
+major          
+minor          
+trivial    
+new_priority   
+
+===== test_priority_add_error_already_exists =====
+Command failed: columns type, name are not unique
+===== test_priority_change_ok =====
+
+Possible Values
+---------------
+blocker        
+critical       
+normal
+minor          
+trivial    
+
+===== test_priority_change_error_bad_priority =====
+Command failed: priority bad_priority does not exist.
+===== test_priority_change_error_bad_new_name =====
+Command failed: columns type, name are not unique
+===== test_priority_remove_ok =====
+
+Possible Values
+---------------
+blocker        
+critical       
+minor          
+trivial    
+
+===== test_priority_remove_error_bad_priority =====
+Command failed: priority bad_priority does not exist.
+===== test_priority_order_down_ok =====
+
+Possible Values
+---------------
+critical
+blocker
+major
+minor
+trivial
+
+===== test_priority_order_up_ok =====
+
+Possible Values
+---------------
+critical
+blocker
+major
+minor
+trivial
+
+===== test_priority_order_error_bad_priority =====
+Command failed: priority bad_priority does not exist.
+===== test_severity_list_ok =====
+
+Possible Values
+---------------
+
+===== test_severity_add_ok =====
+
+Possible Values
+---------------
+new_severity   
+
+===== test_severity_add_error_already_exists =====
+Command failed: columns type, name are not unique
+===== test_severity_change_ok =====
+
+Possible Values 
+----------------
+end-of-the-world
+
+===== test_severity_change_error_bad_severity =====
+Command failed: severity bad_severity does not exist.
+===== test_severity_change_error_bad_new_name =====
+Command failed: columns type, name are not unique
+===== test_severity_remove_ok =====
+
+Possible Values
+---------------
+
+===== test_severity_remove_error_bad_severity =====
+Command failed: severity bad_severity does not exist.
+===== test_severity_order_down_ok =====
+
+Possible Values
+---------------
+bar
+foo
+
+===== test_severity_order_up_ok =====
+
+Possible Values
+---------------
+bar
+foo
+
+===== test_severity_order_error_bad_severity =====
+Command failed: severity bad_severity does not exist.
+===== test_version_list_ok =====
+
+Name  Time
+----------
+2.0       
+1.0       
+
+===== test_version_add_ok =====
+
+Name  Time                    
+----------------
+2.0                           
+1.0                           
+9.9   2004-01-11
+
+===== test_version_add_error_already_exists =====
+Command failed: column name is not unique
+===== test_version_rename_ok =====
+
+Name  Time
+----------
+9.9       
+2.0       
+
+===== test_version_rename_error_bad_version =====
+Command failed: Version bad_version does not exist.
+===== test_version_time_ok =====
+
+Name  Time                    
+----------------
+1.0                           
+2.0   2004-01-11
+
+===== test_version_time_error_bad_version =====
+Command failed: Version bad_version does not exist.
+===== test_version_remove_ok =====
+
+Name  Time
+----------
+2.0       
+
+===== test_version_remove_error_bad_version =====
+Command failed: Version bad_version does not exist.
+===== test_milestone_list_ok =====
+
+Name        Due  Completed
+--------------------------
+milestone1
+milestone2
+milestone3
+milestone4
+
+===== test_milestone_add_ok =====
+
+Name           Due         Completed
+------------------------------------
+new_milestone  2004-01-11
+milestone1
+milestone2
+milestone3
+milestone4
+
+===== test_milestone_add_utf8_ok =====
+
+Name        Due         Completed
+---------------------------------
+état_final  2004-01-11
+milestone1
+milestone2
+milestone3
+milestone4
+
+===== test_milestone_add_error_already_exists =====
+Command failed: column name is not unique
+===== test_milestone_rename_ok =====
+
+Name               Due  Completed
+---------------------------------
+changed_milestone
+milestone2
+milestone3
+milestone4
+
+===== test_milestone_rename_error_bad_milestone =====
+Command failed: Milestone bad_milestone does not exist.
+===== test_milestone_due_ok =====
+
+Name        Due         Completed
+---------------------------------
+milestone2  2004-01-11
+milestone1
+milestone3
+milestone4
+
+===== test_milestone_due_error_bad_milestone =====
+Command failed: Milestone bad_milestone does not exist.
+===== test_milestone_completed_ok =====
+
+Name        Due  Completed
+------------------------------------
+milestone2       2004-01-11 00:00:00
+milestone1
+milestone3
+milestone4
+
+===== test_milestone_completed_error_bad_milestone =====
+Command failed: Milestone bad_milestone does not exist.
+===== test_milestone_remove_ok =====
+
+Name        Due  Completed
+--------------------------
+milestone1
+milestone2
+milestone4
+
+===== test_milestone_remove_error_bad_milestone =====
+Command failed: Milestone bad_milestone does not exist.
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/scripts/tests/admin.py
@@ -0,0 +1,967 @@
+# -*- coding: utf-8 -*-
+# 
+# Copyright (C) 2004-2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Tim Moloney <t.moloney@verizon.net>
+
+import ConfigParser
+import os
+import re
+import shlex
+import sys
+import time
+import unittest
+from StringIO import StringIO
+
+from trac.db_default import data as default_data
+from trac.config import Configuration
+from trac.env import Environment
+from trac.scripts import admin
+from trac.test import InMemoryDatabase
+from trac.util.datefmt import get_date_format_hint
+
+STRIP_TRAILING_SPACE = re.compile(r'( +)$', re.MULTILINE)
+
+
+def load_expected_results(file, pattern):
+    """
+    Reads the file, named file, which contains test results separated by
+    the a regular expression, pattern.  The test results are returned as
+    a dictionary.
+    """
+
+    expected = {}
+    compiled_pattern = re.compile(pattern)
+    f = open(file, 'r')
+    for line in f:
+        line = line.rstrip().decode('utf-8')
+        match = compiled_pattern.search(line)
+        if match:
+            test = match.groups()[0]
+            expected[test] = ''
+        else:
+            expected[test] += line + '\n'
+    f.close()
+    return expected
+
+
+class InMemoryEnvironment(Environment):
+    """
+    A subclass of Environment that keeps its' DB in memory.
+    """
+
+    def get_db_cnx(self):
+        if not hasattr(self, '_db'):
+            self._db = InMemoryDatabase()
+        return self._db
+
+    def create(self, db_str=None):
+        pass
+
+    def verify(self):
+        return True
+
+    def setup_log(self):
+        from trac.log import logger_factory
+        self.log = logger_factory('null')
+
+    def is_component_enabled(self, cls):
+        return cls.__module__.startswith('trac.') and \
+               cls.__module__.find('.tests.') == -1
+
+    def setup_config(self, load_defaults=None):
+        self.config = Configuration(None)
+            
+    def save_config(self):
+        pass
+
+
+class SkipTest(Exception):
+    pass
+
+
+class TracadminTestCase(unittest.TestCase):
+    """
+    Tests the output of trac-admin and is meant to be used with
+    .../trac/tests.py.
+    """
+
+    expected_results = load_expected_results(os.path.join(os.path.split(__file__)[0],
+                                            'admin-tests.txt'),
+                                            '===== (test_[^ ]+) =====')
+
+    def setUp(self):
+        self.env = InMemoryEnvironment('', create=True)
+        self.db = self.env.get_db_cnx()
+
+        self._admin = admin.TracAdmin()
+        self._admin.env_set('', self.env)
+
+        # Set test date to 11th Jan 2004
+        self._test_date = time.strftime('%Y-%m-%d',
+                                        (2004, 1, 11, 0, 0, 0, 6, 1, -1))
+
+    def tearDown(self):
+        self.env = None
+
+    def _execute(self, cmd, strip_trailing_space=True, fail_on_error=False):
+        try:
+            _err = sys.stderr
+            _out = sys.stdout
+            sys.stderr = sys.stdout = out = StringIO()
+            setattr(out, 'encoding', _out.encoding) # fake output encoding
+            retval = None
+            try:
+                retval = self._admin.onecmd(cmd)
+            except SystemExit, e:
+                pass
+            value = out.getvalue()
+            if isinstance(value, str): # reverse what print_listing did
+                value = value.decode(_out.encoding)
+            if strip_trailing_space:
+                return retval, STRIP_TRAILING_SPACE.sub('', value)
+            else:
+                return retval, value
+        finally:
+            sys.stderr = _err
+            sys.stdout = _out
+
+    # About test
+
+    def test_about(self):
+        """
+        Tests the 'about' command in trac-admin.  Since the 'about' command
+        has no command arguments, it is hard to call it incorrectly.  As
+        a result, there is only this one test.
+        """
+
+        from trac import __version__, __license_long__
+
+        expected_results = """
+Trac Admin Console %s
+=================================================================
+%s
+""" % (__version__, __license_long__)
+        rv, output = self._execute('about', strip_trailing_space=False)
+        self.assertEqual(0, rv)
+        self.assertEqual(expected_results, output)
+
+    # Help test
+
+    def test_help_ok(self):
+        """
+        Tests the 'help' command in trac-admin.  Since the 'help' command
+        has no command arguments, it is hard to call it incorrectly.  As
+        a result, there is only this one test.
+        """
+        from trac import __version__
+
+        test_name = sys._getframe().f_code.co_name
+        d = {'version': __version__,
+             'date_format_hint': get_date_format_hint()}
+        expected_results = self.expected_results[test_name] % d
+        rv, output = self._execute('help')
+        self.assertEqual(0, rv)
+        self.assertEqual(expected_results, output)
+
+    # Permission tests
+
+    def test_permission_list_ok(self):
+        """
+        Tests the 'permission list' command in trac-admin.  Since this command
+        has no command arguments, it is hard to call it incorrectly.  As
+        a result, there is only this one test.
+        """
+        test_name = sys._getframe().f_code.co_name
+        try:
+            rv, output = self._execute('permission list')
+            self.assertEqual(0, rv)
+            self.assertEqual(self.expected_results[test_name], output)
+        except SkipTest, e:
+            print>>sys.stderr, 'Skipping test %s: %s' % (test_name, e)
+
+    def test_permission_add_one_action_ok(self):
+        """
+        Tests the 'permission add' command in trac-admin.  This particular
+        test passes valid arguments to add one permission and checks for
+        success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        try:
+            self._execute('permission add test_user WIKI_VIEW')
+            rv, output = self._execute('permission list')
+            self.assertEqual(0, rv)
+            self.assertEqual(self.expected_results[test_name], output)
+        except SkipTest, e:
+            print>>sys.stderr, 'Skipping test %s: %s' % (test_name, e)
+
+    def test_permission_add_multiple_actions_ok(self):
+        """
+        Tests the 'permission add' command in trac-admin.  This particular
+        test passes valid arguments to add multiple permissions and checks for
+        success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        try:
+            self._execute('permission add test_user LOG_VIEW FILE_VIEW')
+            rv, output = self._execute('permission list')
+            self.assertEqual(0, rv)
+            self.assertEqual(self.expected_results[test_name], output)
+        except SkipTest, e:
+            print>>sys.stderr, 'Skipping test %s: %s' % (test_name, e)
+
+    def test_permission_remove_one_action_ok(self):
+        """
+        Tests the 'permission remove' command in trac-admin.  This particular
+        test passes valid arguments to remove one permission and checks for
+        success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        try:
+            self._execute('permission remove anonymous TICKET_MODIFY')
+            rv, output = self._execute('permission list')
+            self.assertEqual(0, rv)
+            self.assertEqual(self.expected_results[test_name], output)
+        except SkipTest, e:
+            print>>sys.stderr, 'Skipping test %s: %s' % (test_name, e)
+
+    def test_permission_remove_multiple_actions_ok(self):
+        """
+        Tests the 'permission remove' command in trac-admin.  This particular
+        test passes valid arguments to remove multiple permission and checks
+        for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        try:
+            test_name = sys._getframe().f_code.co_name
+            self._execute('permission remove anonymous WIKI_CREATE WIKI_MODIFY')
+            rv, output = self._execute('permission list')
+            self.assertEqual(0, rv)
+            self.assertEqual(self.expected_results[test_name], output)
+        except SkipTest, e:
+            print>>sys.stderr, 'Skipping test %s: %s' % (test_name, e)
+
+    # Component tests
+
+    def test_component_list_ok(self):
+        """
+        Tests the 'component list' command in trac-admin.  Since this command
+        has no command arguments, it is hard to call it incorrectly.  As
+        a result, there is only this one test.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('component list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_component_add_ok(self):
+        """
+        Tests the 'component add' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('component add new_component new_user')
+        rv, output = self._execute('component list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_component_add_error_already_exists(self):
+        """
+        Tests the 'component add' command in trac-admin.  This particular
+        test passes a component name that already exists and checks for an
+        error message.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('component add component1 new_user')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_component_rename_ok(self):
+        """
+        Tests the 'component rename' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('component rename component1 changed_name')
+        rv, output = self._execute('component list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_component_rename_error_bad_component(self):
+        """
+        Tests the 'component rename' command in trac-admin.  This particular
+        test tries to rename a component that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('component rename bad_component changed_name')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_component_rename_error_bad_new_name(self):
+        """
+        Tests the 'component rename' command in trac-admin.  This particular
+        test tries to rename a component to a name that already exists.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('component rename component1 component2')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_component_chown_ok(self):
+        """
+        Tests the 'component chown' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('component chown component2 changed_owner')
+        rv, output = self._execute('component list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_component_chown_error_bad_component(self):
+        """
+        Tests the 'component chown' command in trac-admin.  This particular
+        test tries to change the owner of a component that does not
+        exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('component chown bad_component changed_owner')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_component_remove_ok(self):
+        """
+        Tests the 'component remove' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('component remove component1')
+        rv, output = self._execute('component list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_component_remove_error_bad_component(self):
+        """
+        Tests the 'component remove' command in trac-admin.  This particular
+        test tries to remove a component that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('component remove bad_component')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    # Ticket-type tests
+
+    def test_ticket_type_list_ok(self):
+        """
+        Tests the 'ticket_type list' command in trac-admin.  Since this command
+        has no command arguments, it is hard to call it incorrectly.  As
+        a result, there is only this one test.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('ticket_type list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_ticket_type_add_ok(self):
+        """
+        Tests the 'ticket_type add' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('ticket_type add new_type')
+        rv, output = self._execute('ticket_type list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_ticket_type_add_error_already_exists(self):
+        """
+        Tests the 'ticket_type add' command in trac-admin.  This particular
+        test passes a ticket type that already exists and checks for an error
+        message.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('ticket_type add defect')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_ticket_type_change_ok(self):
+        """
+        Tests the 'ticket_type change' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('ticket_type change defect bug')
+        rv, output = self._execute('ticket_type list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_ticket_type_change_error_bad_type(self):
+        """
+        Tests the 'ticket_type change' command in trac-admin.  This particular
+        test tries to change a priority that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('ticket_type change bad_type changed_type')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_ticket_type_change_error_bad_new_name(self):
+        """
+        Tests the 'ticket_type change' command in trac-admin.  This particular
+        test tries to change a ticket type to another type that already exists.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('ticket_type change defect task')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_ticket_type_remove_ok(self):
+        """
+        Tests the 'ticket_type remove' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('ticket_type remove task')
+        rv, output = self._execute('ticket_type list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_ticket_type_remove_error_bad_type(self):
+        """
+        Tests the 'ticket_type remove' command in trac-admin.  This particular
+        test tries to remove a ticket type that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('ticket_type remove bad_type')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_ticket_type_order_down_ok(self):
+        """
+        Tests the 'ticket_type order' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('ticket_type order defect down')
+        rv, output = self._execute('ticket_type list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_ticket_type_order_up_ok(self):
+        """
+        Tests the 'ticket_type order' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('ticket_type order enhancement up')
+        rv, output = self._execute('ticket_type list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_ticket_type_order_error_bad_type(self):
+        """
+        Tests the 'priority order' command in trac-admin.  This particular
+        test tries to reorder a priority that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('ticket_type order bad_type up')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    # Priority tests
+
+    def test_priority_list_ok(self):
+        """
+        Tests the 'priority list' command in trac-admin.  Since this command
+        has no command arguments, it is hard to call it incorrectly.  As
+        a result, there is only this one test.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('priority list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_priority_add_ok(self):
+        """
+        Tests the 'priority add' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('priority add new_priority')
+        rv, output = self._execute('priority list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_priority_add_error_already_exists(self):
+        """
+        Tests the 'priority add' command in trac-admin.  This particular
+        test passes a priority name that already exists and checks for an
+        error message.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('priority add blocker')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_priority_change_ok(self):
+        """
+        Tests the 'priority change' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('priority change major normal')
+        rv, output = self._execute('priority list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_priority_change_error_bad_priority(self):
+        """
+        Tests the 'priority change' command in trac-admin.  This particular
+        test tries to change a priority that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('priority change bad_priority changed_name')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_priority_change_error_bad_new_name(self):
+        """
+        Tests the 'priority change' command in trac-admin.  This particular
+        test tries to change a priority to a name that already exists.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('priority change major minor')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_priority_remove_ok(self):
+        """
+        Tests the 'priority remove' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('priority remove major')
+        rv, output = self._execute('priority list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_priority_remove_error_bad_priority(self):
+        """
+        Tests the 'priority remove' command in trac-admin.  This particular
+        test tries to remove a priority that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('priority remove bad_priority')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_priority_order_down_ok(self):
+        """
+        Tests the 'priority order' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('priority order blocker down')
+        rv, output = self._execute('priority list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_priority_order_up_ok(self):
+        """
+        Tests the 'priority order' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('priority order critical up')
+        rv, output = self._execute('priority list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_priority_order_error_bad_priority(self):
+        """
+        Tests the 'priority order' command in trac-admin.  This particular
+        test tries to reorder a priority that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('priority remove bad_priority')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    # Severity tests
+
+    def test_severity_list_ok(self):
+        """
+        Tests the 'severity list' command in trac-admin.  Since this command
+        has no command arguments, it is hard to call it incorrectly.  As
+        a result, there is only this one test.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('severity list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_severity_add_ok(self):
+        """
+        Tests the 'severity add' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('severity add new_severity')
+        rv, output = self._execute('severity list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_severity_add_error_already_exists(self):
+        """
+        Tests the 'severity add' command in trac-admin.  This particular
+        test passes a severity name that already exists and checks for an
+        error message.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('severity add blocker')
+        rv, output = self._execute('severity add blocker')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_severity_change_ok(self):
+        """
+        Tests the 'severity add' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('severity add critical')
+        self._execute('severity change critical "end-of-the-world"')
+        rv, output = self._execute('severity list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_severity_change_error_bad_severity(self):
+        """
+        Tests the 'severity change' command in trac-admin.  This particular
+        test tries to change a severity that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('severity change bad_severity changed_name')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_severity_change_error_bad_new_name(self):
+        """
+        Tests the 'severity change' command in trac-admin.  This particular
+        test tries to change a severity to a name that already exists.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('severity add major')
+        self._execute('severity add critical')
+        rv, output = self._execute('severity change critical major')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_severity_remove_ok(self):
+        """
+        Tests the 'severity add' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('severity remove trivial')
+        rv, output = self._execute('severity list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_severity_remove_error_bad_severity(self):
+        """
+        Tests the 'severity remove' command in trac-admin.  This particular
+        test tries to remove a severity that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('severity remove bad_severity')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_severity_order_down_ok(self):
+        """
+        Tests the 'severity order' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('severity add foo')
+        self._execute('severity add bar')
+        self._execute('severity order foo down')
+        rv, output = self._execute('severity list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_severity_order_up_ok(self):
+        """
+        Tests the 'severity order' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('severity add foo')
+        self._execute('severity add bar')
+        self._execute('severity order bar up')
+        rv, output = self._execute('severity list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_severity_order_error_bad_severity(self):
+        """
+        Tests the 'severity order' command in trac-admin.  This particular
+        test tries to reorder a priority that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('severity remove bad_severity')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    # Version tests
+
+    def test_version_list_ok(self):
+        """
+        Tests the 'version list' command in trac-admin.  Since this command
+        has no command arguments, it is hard to call it incorrectly.  As
+        a result, there is only this one test.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('version list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_version_add_ok(self):
+        """
+        Tests the 'version add' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('version add 9.9 "%s"' % self._test_date)
+        rv, output = self._execute('version list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_version_add_error_already_exists(self):
+        """
+        Tests the 'version add' command in trac-admin.  This particular
+        test passes a version name that already exists and checks for an
+        error message.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('version add 1.0 "%s"' % self._test_date)
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_version_rename_ok(self):
+        """
+        Tests the 'version rename' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('version rename 1.0 9.9')
+        rv, output = self._execute('version list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_version_rename_error_bad_version(self):
+        """
+        Tests the 'version rename' command in trac-admin.  This particular
+        test tries to rename a version that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('version rename bad_version changed_name')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_version_time_ok(self):
+        """
+        Tests the 'version time' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('version time 2.0 "%s"' % self._test_date)
+        rv, output = self._execute('version list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_version_time_error_bad_version(self):
+        """
+        Tests the 'version time' command in trac-admin.  This particular
+        test tries to change the time on a version that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('version time bad_version "%s"'
+                                   % self._test_date)
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_version_remove_ok(self):
+        """
+        Tests the 'version remove' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('version remove 1.0')
+        rv, output = self._execute('version list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_version_remove_error_bad_version(self):
+        """
+        Tests the 'version remove' command in trac-admin.  This particular
+        test tries to remove a version that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('version remove bad_version')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    # Milestone tests
+
+    def test_milestone_list_ok(self):
+        """
+        Tests the 'milestone list' command in trac-admin.  Since this command
+        has no command arguments, it is hard to call it incorrectly.  As
+        a result, there is only this one test.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('milestone list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_milestone_add_ok(self):
+        """
+        Tests the 'milestone add' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('milestone add new_milestone "%s"' % self._test_date)
+        rv, output = self._execute('milestone list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_milestone_add_utf8_ok(self):
+        """
+        Tests the 'milestone add' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute(u'milestone add \xe9tat_final "%s"'  #\xc3\xa9
+                              % self._test_date)
+        rv, output = self._execute('milestone list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_milestone_add_error_already_exists(self):
+        """
+        Tests the 'milestone add' command in trac-admin.  This particular
+        test passes a milestone name that already exists and checks for an
+        error message.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('milestone add milestone1 "%s"'
+                                   % self._test_date)
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_milestone_rename_ok(self):
+        """
+        Tests the 'milestone rename' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('milestone rename milestone1 changed_milestone')
+        rv, output = self._execute('milestone list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_milestone_rename_error_bad_milestone(self):
+        """
+        Tests the 'milestone rename' command in trac-admin.  This particular
+        test tries to rename a milestone that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('milestone rename bad_milestone changed_name')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_milestone_due_ok(self):
+        """
+        Tests the 'milestone due' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('milestone due milestone2 "%s"' % self._test_date)
+        rv, output = self._execute('milestone list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_milestone_due_error_bad_milestone(self):
+        """
+        Tests the 'milestone due' command in trac-admin.  This particular
+        test tries to change the due date on a milestone that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('milestone due bad_milestone "%s"'
+                                   % self._test_date)
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_milestone_completed_ok(self):
+        """
+        Tests the 'milestone completed' command in trac-admin.  This particular
+        test passes valid arguments and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+
+        self._execute('milestone completed milestone2 "%s"' % self._test_date)
+        rv, output = self._execute('milestone list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_milestone_completed_error_bad_milestone(self):
+        """
+        Tests the 'milestone completed' command in trac-admin.  This particular
+        test tries to change the completed date on a milestone that does not
+        exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('milestone completed bad_milestone "%s"'
+                                   % self._test_date)
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_milestone_remove_ok(self):
+        """
+        Tests the 'milestone remove' command in trac-admin.  This particular
+        test passes a valid argument and checks for success.
+        """
+        test_name = sys._getframe().f_code.co_name
+        self._execute('milestone remove milestone3')
+        rv, output = self._execute('milestone list')
+        self.assertEqual(0, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+    def test_milestone_remove_error_bad_milestone(self):
+        """
+        Tests the 'milestone remove' command in trac-admin.  This particular
+        test tries to remove a milestone that does not exist.
+        """
+        test_name = sys._getframe().f_code.co_name
+        rv, output = self._execute('milestone remove bad_milestone')
+        self.assertEqual(2, rv)
+        self.assertEqual(self.expected_results[test_name], output)
+
+
+def suite():
+    return unittest.makeSuite(TracadminTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100755
--- /dev/null
+++ b/examples/trac/trac/test.py
@@ -0,0 +1,195 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+import unittest
+
+from trac.core import Component, ComponentManager, ExtensionPoint
+from trac.env import Environment
+from trac.db.sqlite_backend import SQLiteConnection
+
+
+def Mock(bases=(), *initargs, **kw):
+    """
+    Simple factory for dummy classes that can be used as replacement for the 
+    real implementation in tests.
+    
+    Base classes for the mock can be specified using the first parameter, which
+    must be either a tuple of class objects or a single class object. If the
+    bases parameter is omitted, the base class of the mock will be object.
+
+    So to create a mock that is derived from the builtin dict type, you can do:
+
+    >>> mock = Mock(dict)
+    >>> mock['foo'] = 'bar'
+    >>> mock['foo']
+    'bar'
+
+    Attributes of the class are provided by any additional keyword parameters.
+
+    >>> mock = Mock(foo='bar')
+    >>> mock.foo
+    'bar'
+
+    Objects produces by this function have the special feature of not requiring
+    the 'self' parameter on methods, because you should keep data at the scope
+    of the test function. So you can just do:
+
+    >>> mock = Mock(add=lambda x,y: x+y)
+    >>> mock.add(1, 1)
+    2
+
+    To access attributes from the mock object from inside a lambda function,
+    just access the mock itself:
+
+    >>> mock = Mock(dict, do=lambda x: 'going to the %s' % mock[x])
+    >>> mock['foo'] = 'bar'
+    >>> mock.do('foo')
+    'going to the bar'
+
+    Because assignments or other types of statements don't work in lambda
+    functions, assigning to a local variable from a mock function requires some
+    extra work:
+
+    >>> myvar = [None]
+    >>> mock = Mock(set=lambda x: myvar.__setitem__(0, x))
+    >>> mock.set(1)
+    >>> myvar[0]
+    1
+    """
+    if not isinstance(bases, tuple):
+        bases = (bases,)
+    cls = type('Mock', bases, {})
+    mock = cls(*initargs)
+    for k,v in kw.items():
+        setattr(mock, k, v)
+    return mock
+
+
+class TestSetup(unittest.TestSuite):
+    """
+    Test suite decorator that allows a fixture to be setup for a complete
+    suite of test cases.
+    """
+    def setUp(self):
+        pass
+
+    def tearDown(self):
+        pass
+
+    def __call__(self, result):
+        self.setUp()
+        unittest.TestSuite.__call__(self, result)
+        self.tearDown()
+        return result
+
+
+class InMemoryDatabase(SQLiteConnection):
+    """
+    DB-API connection object for an SQLite in-memory database, containing all
+    the default Trac tables but no data.
+    """
+    def __init__(self):
+        SQLiteConnection.__init__(self, ':memory:')
+        cursor = self.cnx.cursor()
+
+        from trac.db_default import schema
+        from trac.db.sqlite_backend import _to_sql
+        for table in schema:
+            for stmt in _to_sql(table):
+                cursor.execute(stmt)
+
+        self.cnx.commit()
+
+
+class EnvironmentStub(Environment):
+    """A stub of the trac.env.Environment object for testing."""
+
+    def __init__(self, default_data=False, enable=None):
+        ComponentManager.__init__(self)
+        Component.__init__(self)
+        self.enabled_components = enable
+        self.db = InMemoryDatabase()
+
+        from trac.config import Configuration
+        self.config = Configuration(None)
+
+        from trac.log import logger_factory
+        self.log = logger_factory('test')
+
+        from trac.web.href import Href
+        self.href = Href('/trac.cgi')
+        self.abs_href = Href('http://example.org/trac.cgi')
+
+        from trac import db_default
+        if default_data:
+            cursor = self.db.cursor()
+            for table, cols, vals in db_default.data:
+                cursor.executemany("INSERT INTO %s (%s) VALUES (%s)"
+                                   % (table, ','.join(cols),
+                                      ','.join(['%s' for c in cols])),
+                                   vals)
+            self.db.commit()
+            
+        self.known_users = []
+
+    def is_component_enabled(self, cls):
+        if self.enabled_components is None:
+            return True
+        return cls in self.enabled_components
+
+    def get_db_cnx(self):
+        return self.db
+
+    def get_templates_dir(self):
+        return None
+
+    def get_known_users(self, db):
+        return self.known_users
+
+
+def suite():
+    import trac.tests
+    import trac.db.tests
+    import trac.mimeview.tests
+    import trac.scripts.tests
+    import trac.ticket.tests
+    import trac.util.tests
+    import trac.versioncontrol.tests
+    import trac.versioncontrol.web_ui.tests
+    import trac.web.tests
+    import trac.wiki.tests
+
+    suite = unittest.TestSuite()
+    suite.addTest(trac.tests.suite())
+    suite.addTest(trac.db.tests.suite())
+    suite.addTest(trac.mimeview.tests.suite())
+    suite.addTest(trac.scripts.tests.suite())
+    suite.addTest(trac.ticket.tests.suite())
+    suite.addTest(trac.util.tests.suite())
+    suite.addTest(trac.versioncontrol.tests.suite())
+    suite.addTest(trac.versioncontrol.web_ui.tests.suite())
+    suite.addTest(trac.web.tests.suite())
+    suite.addTest(trac.wiki.tests.suite())
+
+    return suite
+
+if __name__ == '__main__':
+    import doctest, sys
+    doctest.testmod(sys.modules[__name__])
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/tests/__init__.py
@@ -0,0 +1,16 @@
+import unittest
+
+from trac.tests import attachment, config, core, env, perm, wikisyntax
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(attachment.suite())
+    suite.addTest(config.suite())
+    suite.addTest(core.suite())
+    suite.addTest(env.suite())
+    suite.addTest(perm.suite())
+    suite.addTest(wikisyntax.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/tests/allwiki.py
@@ -0,0 +1,22 @@
+import unittest
+
+import trac.tests.wikisyntax
+import trac.ticket.tests.wikisyntax
+import trac.versioncontrol.web_ui.tests.wikisyntax
+import trac.web.tests.wikisyntax
+import trac.wiki.tests.wikisyntax
+import trac.wiki.tests.formatter
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(trac.tests.wikisyntax.suite())
+    suite.addTest(trac.ticket.tests.wikisyntax.suite())
+    suite.addTest(trac.versioncontrol.web_ui.tests.wikisyntax.suite())
+    suite.addTest(trac.web.tests.wikisyntax.suite())
+    suite.addTest(trac.wiki.tests.macros.suite())
+    suite.addTest(trac.wiki.tests.wikisyntax.suite())
+    suite.addTest(trac.wiki.tests.formatter.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/tests/attachment.py
@@ -0,0 +1,207 @@
+# -*- encoding: utf-8 -*-
+from trac.attachment import Attachment, AttachmentModule
+from trac.config import Configuration
+from trac.log import logger_factory
+from trac.test import EnvironmentStub, Mock
+from trac.wiki.formatter import Formatter
+
+import os
+import os.path
+import shutil
+import tempfile
+import unittest
+import time
+
+
+def sleep_for_timestamps():
+    granularity = 0.02
+    time.sleep(granularity)
+    
+
+class AttachmentTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.env.path = os.path.join(tempfile.gettempdir(), 'trac-tempenv')
+        os.mkdir(self.env.path)
+        self.attachments_dir = os.path.join(self.env.path, 'attachments')
+        self.env.config.set('attachment', 'max_size', 512)
+
+        self.perm = Mock(assert_permission=lambda x: None,
+                         has_permission=lambda x: True)
+
+    def tearDown(self):
+        shutil.rmtree(self.env.path)
+
+    def test_get_path(self):
+        attachment = Attachment(self.env, 'ticket', 42)
+        attachment.filename = 'foo.txt'
+        self.assertEqual(os.path.join(self.attachments_dir, 'ticket', '42',
+                                      'foo.txt'),
+                         attachment.path)
+        attachment = Attachment(self.env, 'wiki', 'SomePage')
+        attachment.filename = 'bar.jpg'
+        self.assertEqual(os.path.join(self.attachments_dir, 'wiki', 'SomePage',
+                                      'bar.jpg'),
+                         attachment.path)
+
+    def test_get_path_encoded(self):
+        attachment = Attachment(self.env, 'ticket', 42)
+        attachment.filename = 'Teh foo.txt'
+        self.assertEqual(os.path.join(self.attachments_dir, 'ticket', '42',
+                                      'Teh%20foo.txt'),
+                         attachment.path)
+        attachment = Attachment(self.env, 'wiki', u'ÃœberSicht')
+        attachment.filename = 'Teh bar.jpg'
+        self.assertEqual(os.path.join(self.attachments_dir, 'wiki',
+                                      '%C3%9CberSicht', 'Teh%20bar.jpg'),
+                         attachment.path)
+
+    def test_select_empty(self):
+        self.assertRaises(StopIteration,
+                          Attachment.select(self.env, 'ticket', 42).next)
+        self.assertRaises(StopIteration,
+                          Attachment.select(self.env, 'wiki', 'SomePage').next)
+
+    def test_insert(self):
+        attachment = Attachment(self.env, 'ticket', 42)
+        attachment.insert('foo.txt', tempfile.TemporaryFile(), 0)
+        sleep_for_timestamps()
+        attachment = Attachment(self.env, 'ticket', 42)
+        attachment.insert('bar.jpg', tempfile.TemporaryFile(), 0)
+
+        attachments = Attachment.select(self.env, 'ticket', 42)
+        self.assertEqual('foo.txt', attachments.next().filename)
+        self.assertEqual('bar.jpg', attachments.next().filename)
+        self.assertRaises(StopIteration, attachments.next)
+
+    def test_insert_unique(self):
+        attachment = Attachment(self.env, 'ticket', 42)
+        attachment.insert('foo.txt', tempfile.TemporaryFile(), 0)
+        self.assertEqual('foo.txt', attachment.filename)
+        attachment = Attachment(self.env, 'ticket', 42)
+        attachment.insert('foo.txt', tempfile.TemporaryFile(), 0)
+        self.assertEqual('foo.2.txt', attachment.filename)
+
+    def test_insert_outside_attachments_dir(self):
+        attachment = Attachment(self.env, '../../../../../sth/private', 42)
+        self.assertRaises(AssertionError, attachment.insert, 'foo.txt',
+                          tempfile.TemporaryFile(), 0)
+
+    def test_delete(self):
+        attachment1 = Attachment(self.env, 'wiki', 'SomePage')
+        attachment1.insert('foo.txt', tempfile.TemporaryFile(), 0)
+        attachment2 = Attachment(self.env, 'wiki', 'SomePage')
+        attachment2.insert('bar.jpg', tempfile.TemporaryFile(), 0)
+
+        attachments = Attachment.select(self.env, 'wiki', 'SomePage')
+        self.assertEqual(2, len(list(attachments)))
+
+        attachment1.delete()
+        attachment2.delete()
+
+        assert not os.path.exists(attachment1.path)
+        assert not os.path.exists(attachment2.path)
+
+        attachments = Attachment.select(self.env, 'wiki', 'SomePage')
+        self.assertEqual(0, len(list(attachments)))
+
+    def test_delete_file_gone(self):
+        """
+        Verify that deleting an attachment works even if the referenced file
+        doesn't exist for some reason.
+        """
+        attachment = Attachment(self.env, 'wiki', 'SomePage')
+        attachment.insert('foo.txt', tempfile.TemporaryFile(), 0)
+        os.unlink(attachment.path)
+
+        attachment.delete()
+
+
+class AttachmentModuleTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.env.path = os.path.join(tempfile.gettempdir(), 'trac-tempenv')
+        os.mkdir(self.env.path)
+        self.attachments_dir = os.path.join(self.env.path, 'attachments')
+
+    def tearDown(self):
+        shutil.rmtree(self.env.path)
+
+    def test_wiki_link_wikipage(self):
+        attachment = Attachment(self.env, 'wiki', 'SomePage')
+        attachment.insert('foo.txt', tempfile.TemporaryFile(), 0)
+
+        ns, func = AttachmentModule(self.env).get_link_resolvers().next()
+        self.assertEqual('attachment', ns)
+
+        req = Mock(path_info='/wiki/SomePage')
+        formatter = Formatter(self.env, req)
+        self.assertEqual('<a class="attachment" title="Attachment SomePage: '
+                         'foo.txt" href="/trac.cgi/attachment/wiki/SomePage/'
+                         'foo.txt">Foo</a>',
+                         func(formatter, ns, 'foo.txt', 'Foo'))
+        self.assertEqual('<a class="attachment" title="Attachment SomePage: '
+                         'foo.txt" href="/trac.cgi/attachment/wiki/SomePage/'
+                         'foo.txt?format=raw">Foo</a>',
+                         func(formatter, ns, 'foo.txt?format=raw', 'Foo'))
+
+    def test_wiki_link_subpage(self):
+        attachment = Attachment(self.env, 'wiki', 'SomePage/SubPage')
+        attachment.insert('foo.txt', tempfile.TemporaryFile(), 0)
+
+        ns, func = AttachmentModule(self.env).get_link_resolvers().next()
+        self.assertEqual('attachment', ns)
+
+        req = Mock(path_info='/wiki/SomePage/SubPage')
+        formatter = Formatter(self.env, req)
+        self.assertEqual('<a class="attachment" '
+                         'title="Attachment SomePage/SubPage: foo.txt" '
+                         'href="/trac.cgi/attachment/wiki/SomePage/SubPage/'
+                         'foo.txt">Foo</a>',
+                         func(formatter, ns, 'foo.txt', 'Foo'))
+
+    def test_wiki_link_ticket(self):
+        attachment = Attachment(self.env, 'ticket', 123)
+        attachment.insert('foo.txt', tempfile.TemporaryFile(), 0)
+
+        ns, func = AttachmentModule(self.env).get_link_resolvers().next()
+        self.assertEqual('attachment', ns)
+
+        req = Mock(path_info='/ticket/123')
+        formatter = Formatter(self.env, req)
+        self.assertEqual('<a class="attachment" title="Attachment #123: '
+                         'foo.txt" href="/trac.cgi/attachment/ticket/123/'
+                         'foo.txt">Foo</a>',
+                         func(formatter, ns, 'foo.txt', 'Foo'))
+        self.assertEqual('<a class="attachment" title="Attachment #123: '
+                         'foo.txt" href="/trac.cgi/attachment/ticket/123/'
+                         'foo.txt?format=raw">Foo</a>',
+                         func(formatter, ns, 'foo.txt?format=raw', 'Foo'))
+
+    def test_wiki_link_foreign(self):
+        attachment = Attachment(self.env, 'ticket', 123)
+        attachment.insert('foo.txt', tempfile.TemporaryFile(), 0)
+
+        ns, func = AttachmentModule(self.env).get_link_resolvers().next()
+        self.assertEqual('attachment', ns)
+
+        req = Mock(path_info='/wiki')
+        formatter = Formatter(self.env, req)
+        self.assertEqual('<a class="attachment" title="Attachment #123: '
+                         'foo.txt" href="/trac.cgi/attachment/ticket/123/'
+                         'foo.txt">Foo</a>',
+                         func(formatter, ns, 'ticket:123:foo.txt', 'Foo'))
+        self.assertEqual('<a class="attachment" title="Attachment #123: '
+                         'foo.txt" href="/trac.cgi/attachment/ticket/123/'
+                         'foo.txt?format=raw">Foo</a>',
+                         func(formatter, ns, 'ticket:123:foo.txt?format=raw',
+                              'Foo'))
+
+
+def suite():
+    return unittest.makeSuite(AttachmentTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/tests/config.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+from trac.config import *
+
+import os
+from StringIO import StringIO
+import tempfile
+import time
+import unittest
+
+
+class ConfigurationTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.filename = os.path.join(tempfile.gettempdir(), 'trac-test.ini')
+        self._write([])
+        self._orig_registry = Option.registry
+        Option.registry = {}
+
+    def tearDown(self):
+        Option.registry = self._orig_registry
+        os.remove(self.filename)
+
+    def _write(self, lines):
+        fileobj = open(self.filename, 'w')
+        try:
+            fileobj.write('\n'.join(lines + ['']))
+        finally:
+            fileobj.close()
+
+    def test_default(self):
+        config = Configuration(self.filename)
+        self.assertEquals('', config.get('a', 'option'))
+        self.assertEquals('value', config.get('a', 'option', 'value'))
+
+        class Foo(object):
+            option_a = Option('a', 'option', 'value')
+
+        self.assertEquals('value', config.get('a', 'option'))
+
+    def test_default_bool(self):
+        config = Configuration(self.filename)
+        self.assertEquals(False, config.getbool('a', 'option'))
+        self.assertEquals(True, config.getbool('a', 'option', 'yes'))
+        self.assertEquals(True, config.getbool('a', 'option', 1))
+
+        class Foo(object):
+            option_a = Option('a', 'option', 'true')
+
+        self.assertEquals(True, config.getbool('a', 'option'))
+
+    def test_default_int(self):
+        config = Configuration(self.filename)
+        self.assertRaises(ConfigurationError, config.getint, 'a', 'option')
+        self.assertEquals(1, config.getint('a', 'option', '1'))
+        self.assertEquals(1, config.getint('a', 'option', 1))
+
+        class Foo(object):
+            option_a = Option('a', 'option', '2')
+
+        self.assertEquals(2, config.getint('a', 'option'))
+
+    def test_read_and_get(self):
+        self._write(['[a]', 'option = x'])
+        config = Configuration(self.filename)
+        self.assertEquals('x', config.get('a', 'option'))
+        self.assertEquals('x', config.get('a', 'option', 'y'))
+
+    def test_read_and_getbool(self):
+        self._write(['[a]', 'option = yes'])
+        config = Configuration(self.filename)
+        self.assertEquals(True, config.getbool('a', 'option'))
+        self.assertEquals(True, config.getbool('a', 'option', False))
+
+    def test_read_and_getint(self):
+        self._write(['[a]', 'option = 42'])
+        config = Configuration(self.filename)
+        self.assertEquals(42, config.getint('a', 'option'))
+        self.assertEquals(42, config.getint('a', 'option', 25))
+
+    def test_read_and_getlist(self):
+        self._write(['[a]', 'option = foo, bar, baz'])
+        config = Configuration(self.filename)
+        self.assertEquals(['foo', 'bar', 'baz'],
+                          config.getlist('a', 'option'))
+
+    def test_read_and_getlist_sep(self):
+        self._write(['[a]', 'option = foo | bar | baz'])
+        config = Configuration(self.filename)
+        self.assertEquals(['foo', 'bar', 'baz'],
+                          config.getlist('a', 'option', sep='|'))
+
+    def test_read_and_getlist_keep_empty(self):
+        self._write(['[a]', 'option = ,bar,baz'])
+        config = Configuration(self.filename)
+        self.assertEquals(['bar', 'baz'], config.getlist('a', 'option'))
+        self.assertEquals(['', 'bar', 'baz'],
+                          config.getlist('a', 'option', keep_empty=True))
+
+    def test_set_and_save(self):
+        config = Configuration(self.filename)
+        config.set('a', 'option', 'x')
+        self.assertEquals('x', config.get('a', 'option'))
+        config.save()
+
+        configfile = open(self.filename, 'r')
+        self.assertEquals(['[a]\n', 'option = x\n', '\n'],
+                          configfile.readlines())
+        configfile.close()
+
+    def test_sections(self):
+        self._write(['[a]', 'option = x', '[b]', 'option = y'])
+        config = Configuration(self.filename)
+        self.assertEquals(['a', 'b'], config.sections())
+
+    def test_options(self):
+        self._write(['[a]', 'option = x', '[b]', 'option = y'])
+        config = Configuration(self.filename)
+        self.assertEquals(('option', 'x'), iter(config.options('a')).next())
+        self.assertEquals(('option', 'y'), iter(config.options('b')).next())
+        self.assertRaises(StopIteration, iter(config.options('c')).next)
+
+    def test_reparse(self):
+        self._write(['[a]', 'option = x'])
+        config = Configuration(self.filename)
+        self.assertEquals('x', config.get('a', 'option'))
+        time.sleep(1) # needed because of low mtime granularity
+
+        self._write(['[a]', 'option = y'])
+        config.parse_if_needed()
+        self.assertEquals('y', config.get('a', 'option'))
+
+
+def suite():
+    return unittest.makeSuite(ConfigurationTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/tests/core.py
@@ -0,0 +1,289 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+from trac.core import *
+
+import unittest
+
+
+class ITest(Interface):
+    def test():
+        """Dummy function."""
+
+
+class ComponentTestCase(unittest.TestCase):
+
+    def setUp(self):
+        from trac.core import ComponentManager, ComponentMeta
+        self.compmgr = ComponentManager()
+
+        # Make sure we have no external components hanging around in the
+        # component registry
+        self.old_registry = ComponentMeta._registry
+        ComponentMeta._registry = {}
+
+    def tearDown(self):
+        # Restore the original component registry
+        from trac.core import ComponentMeta
+        ComponentMeta._registry = self.old_registry
+
+    def test_base_class_not_registered(self):
+        """
+        Make sure that the Component base class does not appear in the component
+        registry.
+        """
+        from trac.core import ComponentMeta
+        assert Component not in ComponentMeta._components
+        self.assertRaises(TracError, self.compmgr.__getitem__, Component)
+
+    def test_abstract_component_not_registered(self):
+        """
+        Make sure that a Component class marked as abstract does not appear in
+        the component registry.
+        """
+        from trac.core import ComponentMeta
+        class AbstractComponent(Component):
+            abstract = True
+        assert AbstractComponent not in ComponentMeta._components
+        self.assertRaises(TracError, self.compmgr.__getitem__,
+                          AbstractComponent)
+
+    def test_unregistered_component(self):
+        """
+        Make sure the component manager refuses to manage classes not derived
+        from `Component`.
+        """
+        class NoComponent(object): pass
+        self.assertRaises(TracError, self.compmgr.__getitem__, NoComponent)
+
+    def test_component_registration(self):
+        """
+        Verify that classes derived from `Component` are managed by the
+        component manager.
+        """
+        class ComponentA(Component):
+            pass
+        assert self.compmgr[ComponentA]
+        assert ComponentA(self.compmgr)
+
+    def test_component_identity(self):
+        """
+        Make sure instantiating a component multiple times just returns the
+        same instance again.
+        """
+        class ComponentA(Component):
+            pass
+        c1 = ComponentA(self.compmgr)
+        c2 = ComponentA(self.compmgr)
+        assert c1 is c2, 'Expected same component instance'
+        c2 = self.compmgr[ComponentA]
+        assert c1 is c2, 'Expected same component instance'
+
+    def test_component_initializer(self):
+        """
+        Makes sure that a components' `__init__` method gets called.
+        """
+        class ComponentA(Component):
+            def __init__(self):
+                self.data = 'test'
+        self.assertEqual('test', ComponentA(self.compmgr).data)
+        ComponentA(self.compmgr).data = 'newtest'
+        self.assertEqual('newtest', ComponentA(self.compmgr).data)
+
+    def test_inherited_component_initializer(self):
+        """
+        Makes sure that a the `__init__` method of a components' super-class
+        gets called if the component doesn't override it.
+        """
+        class ComponentA(Component):
+            def __init__(self):
+                self.data = 'foo'
+        class ComponentB(ComponentA):
+            def __init__(self):
+                self.data = 'bar'
+        class ComponentC(ComponentB):
+            pass
+        self.assertEqual('bar', ComponentC(self.compmgr).data)
+        ComponentC(self.compmgr).data = 'baz'
+        self.assertEqual('baz', ComponentC(self.compmgr).data)
+
+    def test_implements_called_outside_classdef(self):
+        """
+        Verify that calling implements() outside a class definition raises an
+        `AssertionError`.
+        """
+        try:
+            implements()
+            self.fail('Expected AssertionError')
+        except AssertionError:
+            pass
+
+    def test_implements_called_twice(self):
+        """
+        Verify that calling implements() twice in a class definition raises an
+        `AssertionError`.
+        """
+        try:
+            class ComponentA(Component):
+                implements()
+                implements()
+            self.fail('Expected AssertionError')
+        except AssertionError:
+            pass
+
+    def test_attribute_access(self):
+        """
+        Verify that accessing undefined attributes on components raises an
+        `AttributeError`.
+        """
+        class ComponentA(Component):
+            pass
+        comp = ComponentA(self.compmgr)
+        try:
+            comp.foo
+            self.fail('Expected AttributeError')
+        except AttributeError:
+            pass
+
+    def test_nonconforming_extender(self):
+        """
+        Verify that accessing a method of a declared extension point interface 
+        raises a normal `AttributeError` if the component does not implement
+        the method.
+        """
+        class ComponentA(Component):
+            tests = ExtensionPoint(ITest)
+        class ComponentB(Component):
+            implements(ITest)
+        tests = iter(ComponentA(self.compmgr).tests)
+        try:
+            tests.next().test()
+            self.fail('Expected AttributeError')
+        except AttributeError:
+            pass
+
+    def test_extension_point_with_no_extension(self):
+        """
+        Verify that accessing an extension point with no extenders returns an
+        empty list.
+        """
+        class ComponentA(Component):
+            tests = ExtensionPoint(ITest)
+        tests = iter(ComponentA(self.compmgr).tests)
+        self.assertRaises(StopIteration, tests.next)
+
+    def test_extension_point_with_one_extension(self):
+        """
+        Verify that a single component extending an extension point can be
+        accessed through the extension point attribute of the declaring
+        component.
+        """
+        class ComponentA(Component):
+            tests = ExtensionPoint(ITest)
+        class ComponentB(Component):
+            implements(ITest)
+            def test(self): return 'x'
+        tests = iter(ComponentA(self.compmgr).tests)
+        self.assertEquals('x', tests.next().test())
+        self.assertRaises(StopIteration, tests.next)
+
+    def test_extension_point_with_two_extensions(self):
+        """
+        Verify that two components extending an extension point can be accessed
+        through the extension point attribute of the declaring component.
+        """
+        class ComponentA(Component):
+            tests = ExtensionPoint(ITest)
+        class ComponentB(Component):
+            implements(ITest)
+            def test(self): return 'x'
+        class ComponentC(Component):
+            implements(ITest)
+            def test(self): return 'y'
+        tests = iter(ComponentA(self.compmgr).tests)
+        self.assertEquals('x', tests.next().test())
+        self.assertEquals('y', tests.next().test())
+        self.assertRaises(StopIteration, tests.next)
+
+    def test_inherited_extension_point(self):
+        """
+        Verify that extension points are inherited to sub-classes.
+        """
+        class BaseComponent(Component):
+            tests = ExtensionPoint(ITest)
+        class ConcreteComponent(BaseComponent):
+            pass
+        class ExtendingComponent(Component):
+            implements(ITest)
+            def test(self): return 'x'
+        tests = iter(ConcreteComponent(self.compmgr).tests)
+        self.assertEquals('x', tests.next().test())
+        self.assertRaises(StopIteration, tests.next)
+
+    def test_inherited_implements(self):
+        """
+        Verify that a component with a super-class implementing an extension
+        point interface is also registered as implementing that interface.
+        """
+        class BaseComponent(Component):
+            implements(ITest)
+            abstract = True
+        class ConcreteComponent(BaseComponent):
+            pass
+        from trac.core import ComponentMeta
+        assert ConcreteComponent in ComponentMeta._registry[ITest]
+
+    def test_component_manager_component(self):
+        """
+        Verify that a component manager can itself be a component with its own
+        extension points.
+        """
+        from trac.core import ComponentManager
+        class ManagerComponent(ComponentManager, Component):
+            tests = ExtensionPoint(ITest)
+            def __init__(self, foo, bar):
+                ComponentManager.__init__(self)
+                self.foo, self.bar = foo, bar
+        class Extender(Component):
+            implements(ITest)
+            def test(self): return 'x'
+        mgr = ManagerComponent('Test', 42)
+        assert id(mgr) == id(mgr[ManagerComponent])
+        tests = iter(mgr.tests)
+        self.assertEquals('x', tests.next().test())
+        self.assertRaises(StopIteration, tests.next)
+
+    def test_instantiation_doesnt_enable(self):
+        """
+        Make sure that a component disabled by the ComponentManager is not
+        implicitly enabled by instantiating it directly.
+        """
+        from trac.core import ComponentManager
+        class DisablingComponentManager(ComponentManager):
+            def is_component_enabled(self, cls):
+                return False
+        class ComponentA(Component):
+            pass
+        mgr = DisablingComponentManager()
+        instance = ComponentA(mgr)
+        self.assertEqual(None, mgr[ComponentA])
+
+
+def suite():
+    return unittest.makeSuite(ComponentTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/tests/env.py
@@ -0,0 +1,51 @@
+from trac import db_default
+from trac.env import Environment
+
+import os.path
+import unittest
+import tempfile
+import shutil
+
+
+class EnvironmentTestCase(unittest.TestCase):
+
+    def setUp(self):
+        env_path = os.path.join(tempfile.gettempdir(), 'trac-tempenv')
+        self.env = Environment(env_path, create=True)
+        self.db = self.env.get_db_cnx()
+
+    def tearDown(self):
+        self.db.close()
+        self.env.shutdown() # really closes the db connections
+        shutil.rmtree(self.env.path)
+
+    def test_get_version(self):
+        """Testing env.get_version"""
+        assert self.env.get_version() == db_default.db_version
+
+    def test_get_known_users(self):
+        """Testing env.get_known_users"""
+        cursor = self.db.cursor()
+        cursor.executemany("INSERT INTO session VALUES (%s,%s,0)",
+                           [('123', 0),('tom', 1), ('joe', 1), ('jane', 1)])
+        cursor.executemany("INSERT INTO session_attribute VALUES (%s,%s,%s,%s)",
+                           [('123', 0, 'email', 'a@example.com'),
+                            ('tom', 1, 'name', 'Tom'),
+                            ('tom', 1, 'email', 'tom@example.com'),
+                            ('joe', 1, 'email', 'joe@example.com'),
+                            ('jane', 1, 'name', 'Jane')])
+        users = {}
+        for username,name,email in self.env.get_known_users(self.db):
+            users[username] = (name, email)
+
+        assert not users.has_key('anonymous')
+        self.assertEqual(('Tom', 'tom@example.com'), users['tom'])
+        self.assertEqual((None, 'joe@example.com'), users['joe'])
+        self.assertEqual(('Jane', None), users['jane'])
+
+
+def suite():
+    return unittest.makeSuite(EnvironmentTestCase,'test')
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/tests/notification.py
@@ -0,0 +1,430 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2006 Edgewall Software
+# Copyright (C) 2005-2006 Emmanuel Blot <emmanuel.blot@free.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Include a basic SMTP server, based on L. Smithson 
+# (lsmithson@open-networks.co.uk) extensible Python SMTP Server
+#
+# This file does not contain unit tests, but provides a set of
+# classes to run SMTP notification tests
+#
+
+import socket
+import string
+import threading
+import re
+import base64
+import quopri
+
+
+LF = '\n'
+CR = '\r'
+email_re = re.compile(r"([\w\d_\.\-])+\@(([\w\d\-])+\.)+([\w\d]{2,4})+")
+header_re = re.compile(r'^=\?(?P<charset>[\w\d\-]+)\?(?P<code>[qb])\?(?P<value>.*)\?=$')
+
+
+class SMTPServerInterface:
+    """
+    A base class for the imlementation of an application specific SMTP
+    Server. Applications should subclass this and overide these
+    methods, which by default do nothing.
+
+    A method is defined for each RFC821 command. For each of these
+    methods, 'args' is the complete command received from the
+    client. The 'data' method is called after all of the client DATA
+    is received.
+
+    If a method returns 'None', then a '250 OK'message is
+    automatically sent to the client. If a subclass returns a non-null
+    string then it is returned instead.
+    """
+
+    def helo(self, args):
+        return None
+    
+    def mail_from(self, args):
+        return None
+        
+    def rcpt_to(self, args):
+        return None
+
+    def data(self, args):
+        return None
+
+    def quit(self, args):
+        return None
+
+    def reset(self, args):
+        return None
+    
+#
+# Some helper functions for manipulating from & to addresses etc.
+#
+
+def strip_address(address):
+    """
+    Strip the leading & trailing <> from an address.  Handy for
+    getting FROM: addresses.
+    """
+    start = string.index(address, '<') + 1
+    end = string.index(address, '>')
+    return address[start:end]
+
+def split_to(address):
+    """
+    Return 'address' as undressed (host, fulladdress) tuple.
+    Handy for use with TO: addresses.
+    """
+    start = string.index(address, '<') + 1
+    sep = string.index(address, '@') + 1
+    end = string.index(address, '>')
+    return (address[sep:end], address[start:end],)
+
+
+#
+# This drives the state for a single RFC821 message.
+#
+class SMTPServerEngine:
+    """
+    Server engine that calls methods on the SMTPServerInterface object
+    passed at construction time. It is constructed with a bound socket
+    connection to a client. The 'chug' method drives the state,
+    returning when the client RFC821 transaction is complete. 
+    """
+
+    ST_INIT = 0
+    ST_HELO = 1
+    ST_MAIL = 2
+    ST_RCPT = 3
+    ST_DATA = 4
+    ST_QUIT = 5
+    
+    def __init__(self, socket, impl):
+        self.impl = impl;
+        self.socket = socket;
+        self.state = SMTPServerEngine.ST_INIT
+
+    def chug(self):
+        """
+        Chug the engine, till QUIT is received from the client. As
+        each RFC821 message is received, calls are made on the
+        SMTPServerInterface methods on the object passed at
+        construction time.
+        """
+        self.socket.send("220 Welcome to Trac notification test server\r\n")
+        while 1:
+            data = ''
+            completeLine = 0
+            # Make sure an entire line is received before handing off
+            # to the state engine. Thanks to John Hall for pointing
+            # this out.
+            while not completeLine:
+                try:
+                    lump = self.socket.recv(1024);
+                    if len(lump):
+                        data += lump
+                        if (len(data) >= 2) and data[-2:] == '\r\n':
+                            completeLine = 1
+                            if self.state != SMTPServerEngine.ST_DATA:
+                                rsp, keep = self.do_command(data)
+                            else:
+                                rsp = self.do_data(data)
+                                if rsp == None:
+                                    continue
+                            self.socket.send(rsp + "\r\n")
+                            if keep == 0:
+                                self.socket.close()
+                                return
+                    else:
+                        # EOF
+                        return
+                except socket.error:
+                    return
+        return
+            
+    def do_command(self, data):
+        """Process a single SMTP Command"""
+        cmd = data[0:4]
+        cmd = string.upper(cmd)
+        keep = 1
+        rv = None
+        if cmd == "HELO":
+            self.state = SMTPServerEngine.ST_HELO
+            rv = self.impl.helo(data[5:])
+        elif cmd == "RSET":
+            rv = self.impl.reset(data[5:])
+            self.data_accum = ""
+            self.state = SMTPServerEngine.ST_INIT
+        elif cmd == "NOOP":
+            pass
+        elif cmd == "QUIT":
+            rv = self.impl.quit(data[5:])
+            keep = 0
+        elif cmd == "MAIL":
+            if self.state != SMTPServerEngine.ST_HELO:
+                return ("503 Bad command sequence", 1)
+            self.state = SMTPServerEngine.ST_MAIL
+            rv = self.impl.mail_from(data[5:])
+        elif cmd == "RCPT":
+            if (self.state != SMTPServerEngine.ST_MAIL) and \
+               (self.state != SMTPServerEngine.ST_RCPT):
+                return ("503 Bad command sequence", 1)
+            self.state = SMTPServerEngine.ST_RCPT
+            rv = self.impl.rcpt_to(data[5:])
+        elif cmd == "DATA":
+            if self.state != SMTPServerEngine.ST_RCPT:
+                return ("503 Bad command sequence", 1)
+            self.state = SMTPServerEngine.ST_DATA
+            self.data_accum = ""
+            return ("354 OK, Enter data, terminated with a \\r\\n.\\r\\n", 1)
+        else:
+            return ("505 Eh? WTF was that?", 1)
+
+        if rv:
+            return (rv, keep)
+        else:
+            return("250 OK", keep)
+
+    def do_data(self, data):
+        """
+        Process SMTP Data. Accumulates client DATA until the
+        terminator is found.
+        """
+        self.data_accum = self.data_accum + data
+        if len(self.data_accum) > 4 and self.data_accum[-5:] == '\r\n.\r\n':
+            self.data_accum = self.data_accum[:-5]
+            rv = self.impl.data(self.data_accum)
+            self.state = SMTPServerEngine.ST_HELO
+            if rv:
+                return rv
+            else:
+                return "250 OK - Data and terminator. found"
+        else:
+            return None
+
+
+class SMTPServer:
+    """
+    A single threaded SMTP Server connection manager. Listens for
+    incoming SMTP connections on a given port. For each connection,
+    the SMTPServerEngine is chugged, passing the given instance of
+    SMTPServerInterface. 
+    """
+    
+    def __init__(self, port):
+        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        self._socket.bind(("", port))
+        self._socket_service = None
+
+    def serve(self, impl):
+        while ( self._resume ):
+            try:
+                nsd = self._socket.accept()
+            except socket.error:
+                return
+            self._socket_service = nsd[0]
+            engine = SMTPServerEngine(self._socket_service, impl)
+            engine.chug()
+            self._socket_service = None
+
+    def start(self):
+        self._socket.listen(1)
+        self._resume = True        
+        
+    def stop(self):
+        self._resume = False
+        
+    def terminate(self):
+        if self._socket_service:
+            # force the blocking socket to stop waiting for data 
+            try:
+                #self._socket_service.shutdown(2)
+                self._socket_service.close()
+            except AttributeError:
+                # the SMTP server may also discard the socket
+                pass
+            self._socket_service = None
+        if self._socket:
+            #self._socket.shutdown(2)
+            self._socket.close()
+            self._socket = None
+
+class SMTPServerStore(SMTPServerInterface):
+    """
+    Simple store for SMTP data
+    """
+
+    def __init__(self):
+        self.reset(None)
+
+    def helo(self, args):
+        self.reset(None)
+    
+    def mail_from(self, args):
+        if args.lower().startswith('from:'):
+            self.sender = strip_address(args[5:].replace('\r\n','').strip())
+        
+    def rcpt_to(self, args):
+        if args.lower().startswith('to:'):
+            rcpt = args[3:].replace('\r\n','').strip()
+            self.recipients.append(strip_address(rcpt))
+
+    def data(self, args):
+        self.message = args
+
+    def quit(self, args):
+        pass
+
+    def reset(self, args):
+        self.sender = None
+        self.recipients = []
+        self.message = None
+        
+
+class SMTPThreadedServer(threading.Thread):
+    """
+    Run a SMTP server for a single connection, within a dedicated thread
+    """
+
+    def __init__(self, port):
+        self.port = port
+        self.server = SMTPServer(port)
+        self.store  = SMTPServerStore()
+        threading.Thread.__init__(self)
+      
+    def run(self):
+        # run from within the SMTP server thread
+        self.server.serve(impl = self.store)
+
+    def start(self):
+        # run from the main thread
+        self.server.start()
+        threading.Thread.start(self)
+        
+    def stop(self):
+        # run from the main thread
+        self.server.stop()
+        # send a message to make the SMTP server quit gracefully
+        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        try:
+            s.connect(('localhost', self.port))
+            r = s.send("QUIT\r\n");
+        except socket.error:
+            pass
+        s.close()
+        # wait for the SMTP server to complete (for up to 2 secs)
+        self.join(2.0)
+        # clean up the SMTP server (and force quit if needed)
+        self.server.terminate()
+
+    def get_sender(self):
+        return self.store.sender
+
+    def get_recipients(self):
+        return self.store.recipients
+
+    def get_message(self):
+        return self.store.message
+        
+    def cleanup(self):
+        self.store.reset(None)
+
+def smtp_address(fulladdr):
+    mo = email_re.search(fulladdr)
+    if mo:
+        return mo.group(0)
+    if start >= 0:
+        return fulladdr[start+1:-1]
+    return fulladdr
+
+def decode_header(header):
+    """ Decode a MIME-encoded header value """
+    mo = header_re.match(header)
+    # header does not seem to be MIME-encoded
+    if not mo:
+        return header
+    # attempts to decode the hedear, 
+    # following the specified MIME endoding and charset
+    try:
+        encoding = mo.group('code').lower() 
+        if encoding  == 'q':
+            val = quopri.decodestring(mo.group('value'), header=True)
+        elif encoding == 'b':
+            val = base64.decodestring(mo.group('value'))
+        else:
+            raise AssertionError, "unsupported encoding: %s" % encoding
+        header = unicode(val, mo.group('charset'))
+    except Exception, e:
+        raise AssertionError, e
+    return header
+
+def parse_smtp_message(msg):
+    """ Split a SMTP message into its headers and body.
+        Returns a (headers, body) tuple 
+        We do not use the email/MIME Python facilities here
+        as they may accept invalid RFC822 data, or data we do not
+        want to support nor generate """
+    headers = {}
+    lh = None
+    body = None
+    # last line does not contain the final line ending
+    msg += '\r\n'
+    for line in msg.splitlines(True):
+        if body != None:
+            # append current line to the body
+            if line[-2] == CR:
+                body += line[0:-2]
+                body += '\n'
+            else:
+                raise AssertionError, "body misses CRLF: %s (0x%x)" \
+                                      % (line, ord(line[-1]))
+        else:
+            if line[-2] != CR:
+                # RFC822 requires CRLF at end of field line
+                raise AssertionError, "header field misses CRLF: %s (0x%x)" \
+                                      % (line, ord(line[-1]))
+            # discards CR
+            line = line[0:-2]
+            if line.strip() == '':
+                # end of headers, body starts
+                body = '' 
+            else:
+                val = None
+                if line[0] in ' \t':
+                    # continution of the previous line
+                    if not lh:
+                        # unexpected multiline
+                        raise AssertionError, \
+                             "unexpected folded line: %s" % line
+                    val = decode_header(line.strip(' \t'))
+                    # appends the current line to the previous one
+                    if not isinstance(headers[lh], tuple):
+                        headers[lh] += val
+                    else:
+                        headers[lh][-1] = headers[lh][-1] + val
+                else:
+                    # splits header name from value
+                    (h,v) = line.split(':',1)
+                    val = decode_header(v.strip())
+                    if headers.has_key(h):
+                        if isinstance(headers[h], tuple):
+                            headers[h] += val
+                        else:
+                            headers[h] = (headers[h], val)
+                    else:
+                        headers[h] = val
+                    # stores the last header (for multilines headers)
+                    lh = h
+    # returns the headers and the message body
+    return (headers, body)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/tests/perm.py
@@ -0,0 +1,157 @@
+from trac import perm
+from trac.config import Configuration
+from trac.core import *
+from trac.test import EnvironmentStub
+
+import unittest
+
+
+class DefaultPermissionStoreTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub(enable=[perm.DefaultPermissionStore,
+                                           perm.DefaultPermissionGroupProvider])
+        self.store = perm.DefaultPermissionStore(self.env)
+
+    def test_simple_actions(self):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.executemany("INSERT INTO permission VALUES (%s,%s)", [
+                           ('john', 'WIKI_MODIFY'), ('john', 'REPORT_ADMIN'),
+                           ('kate', 'TICKET_CREATE')])
+        self.assertEquals(['WIKI_MODIFY', 'REPORT_ADMIN'],
+                          self.store.get_user_permissions('john'))
+        self.assertEquals(['TICKET_CREATE'], self.store.get_user_permissions('kate'))
+
+    def test_simple_group(self):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.executemany("INSERT INTO permission VALUES (%s,%s)", [
+                           ('dev', 'WIKI_MODIFY'), ('dev', 'REPORT_ADMIN'),
+                           ('john', 'dev')])
+        self.assertEquals(['WIKI_MODIFY', 'REPORT_ADMIN'],
+                          self.store.get_user_permissions('john'))
+
+    def test_nested_groups(self):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.executemany("INSERT INTO permission VALUES (%s,%s)", [
+                           ('dev', 'WIKI_MODIFY'), ('dev', 'REPORT_ADMIN'),
+                           ('admin', 'dev'), ('john', 'admin')])
+        self.assertEquals(['WIKI_MODIFY', 'REPORT_ADMIN'],
+                          self.store.get_user_permissions('john'))
+
+    def test_builtin_groups(self):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.executemany("INSERT INTO permission VALUES (%s,%s)", [
+                           ('authenticated', 'WIKI_MODIFY'),
+                           ('authenticated', 'REPORT_ADMIN'),
+                           ('anonymous', 'TICKET_CREATE')])
+        self.assertEquals(['WIKI_MODIFY', 'REPORT_ADMIN', 'TICKET_CREATE'],
+                          self.store.get_user_permissions('john'))
+        self.assertEquals(['TICKET_CREATE'],
+                          self.store.get_user_permissions('anonymous'))
+
+    def test_get_all_permissions(self):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.executemany("INSERT INTO permission VALUES (%s,%s)", [
+                           ('dev', 'WIKI_MODIFY'), ('dev', 'REPORT_ADMIN'),
+                           ('john', 'dev')])
+        expected = [('dev', 'WIKI_MODIFY'),
+                    ('dev', 'REPORT_ADMIN'),
+                    ('john', 'dev')]
+        for res in self.store.get_all_permissions():
+            self.failIf(res not in expected)
+
+
+class TestPermissionRequestor(Component):
+    implements(perm.IPermissionRequestor)
+
+    def get_permission_actions(self):
+        return ['TEST_CREATE', 'TEST_DELETE', 'TEST_MODIFY',
+                ('TEST_ADMIN', ['TEST_CREATE', 'TEST_DELETE',
+                                'TEST_MODIFY'])]
+
+class PermissionSystemTestCase(unittest.TestCase):
+
+    def setUp(self):
+        from trac.core import ComponentMeta
+
+        self.env = EnvironmentStub(enable=[perm.PermissionSystem,
+                                           perm.DefaultPermissionStore,
+                                           TestPermissionRequestor])
+        self.perm = perm.PermissionSystem(self.env)
+
+    def test_all_permissions(self):
+        self.assertEqual({'TEST_CREATE': True, 'TEST_DELETE': True,
+                          'TEST_MODIFY': True,  'TEST_ADMIN': True,
+                          'TRAC_ADMIN': True},
+                         self.perm.get_user_permissions())
+
+    def test_simple_permissions(self):
+        self.perm.grant_permission('bob', 'TEST_CREATE')
+        self.perm.grant_permission('jane', 'TEST_DELETE')
+        self.perm.grant_permission('jane', 'TEST_MODIFY')
+        self.assertEqual({'TEST_CREATE': True},
+                         self.perm.get_user_permissions('bob'))
+        self.assertEqual({'TEST_DELETE': True, 'TEST_MODIFY': True},
+                         self.perm.get_user_permissions('jane'))
+
+    def test_meta_permissions(self):
+        self.perm.grant_permission('bob', 'TEST_CREATE')
+        self.perm.grant_permission('jane', 'TEST_ADMIN')
+        self.assertEqual({'TEST_CREATE': True},
+                         self.perm.get_user_permissions('bob'))
+        self.assertEqual({'TEST_CREATE': True, 'TEST_DELETE': True,
+                          'TEST_MODIFY': True,  'TEST_ADMIN': True},
+                         self.perm.get_user_permissions('jane'))
+
+    def test_get_all_permissions(self):
+        self.perm.grant_permission('bob', 'TEST_CREATE')
+        self.perm.grant_permission('jane', 'TEST_ADMIN')
+        expected = [('bob', 'TEST_CREATE'),
+                    ('jane', 'TEST_ADMIN')]
+        for res in self.perm.get_all_permissions():
+            self.failIf(res not in expected)
+
+
+class PermTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub(enable=[perm.PermissionSystem,
+                                           perm.DefaultPermissionStore,
+                                           TestPermissionRequestor])
+        # Add a few groups
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.executemany("INSERT INTO permission VALUES(%s,%s)", [
+                           ('employee', 'TEST_MODIFY'),
+                           ('developer', 'TEST_ADMIN'),
+                           ('developer', 'employee'),
+                           ('bob', 'developer')])
+        db.commit()
+        self.perm = perm.PermissionCache(self.env, 'bob')
+
+    def test_has_permission(self):
+        self.assertEqual(1, self.perm.has_permission('TEST_MODIFY'))
+        self.assertEqual(1, self.perm.has_permission('TEST_ADMIN'))
+        self.assertEqual(0, self.perm.has_permission('TRAC_ADMIN'))
+
+    def test_assert_permission(self):
+        self.perm.assert_permission('TEST_MODIFY')
+        self.perm.assert_permission('TEST_ADMIN')
+        self.assertRaises(perm.PermissionError,
+                          self.perm.assert_permission, 'TRAC_ADMIN')
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(DefaultPermissionStoreTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(PermissionSystemTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(PermTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/tests/wikisyntax.py
@@ -0,0 +1,42 @@
+import unittest
+
+from trac.Search import SearchModule
+from trac.wiki.tests import formatter
+
+SEARCH_TEST_CASES="""
+============================== search: link resolver
+search:foo
+search:"foo bar"
+[search:bar Bar]
+[search:bar]
+[search:]
+------------------------------
+<p>
+<a class="search" href="/search?q=foo">search:foo</a>
+<a class="search" href="/search?q=foo+bar">search:"foo bar"</a>
+<a class="search" href="/search?q=bar">Bar</a>
+<a class="search" href="/search?q=bar">bar</a>
+<a class="search" href="/search?q=">search</a>
+</p>
+------------------------------
+============================== search: link resolver with query arguments
+search:?q=foo&wiki=on
+search:"?q=foo bar&wiki=on"
+[search:?q=bar&ticket=on Bar in Tickets]
+------------------------------
+<p>
+<a class="search" href="/search?q=foo&amp;wiki=on">search:?q=foo&amp;wiki=on</a>
+<a class="search" href="/search?q=foo+bar&amp;wiki=on">search:"?q=foo bar&amp;wiki=on"</a>
+<a class="search" href="/search?q=bar&amp;ticket=on">Bar in Tickets</a>
+</p>
+------------------------------
+"""
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(formatter.suite(SEARCH_TEST_CASES, file=__file__))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/__init__.py
@@ -0,0 +1,2 @@
+from trac.ticket.api import *
+from trac.ticket.model import *
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/api.py
@@ -0,0 +1,270 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+
+import re
+
+from trac.config import *
+from trac.core import *
+from trac.perm import IPermissionRequestor, PermissionSystem
+from trac.Search import ISearchSource, search_to_sql, shorten_result
+from trac.util.text import shorten_line
+from trac.util.markup import html, Markup
+from trac.wiki import IWikiSyntaxProvider, Formatter
+
+
+class ITicketChangeListener(Interface):
+    """Extension point interface for components that require notification when
+    tickets are created, modified, or deleted."""
+
+    def ticket_created(ticket):
+        """Called when a ticket is created."""
+
+    def ticket_changed(ticket, comment, old_values):
+        """Called when a ticket is modified.
+        
+        `old_values` is a dictionary containing the previous values of the
+        fields that have changed.
+        """
+
+    def ticket_deleted(ticket):
+        """Called when a ticket is deleted."""
+
+
+class ITicketManipulator(Interface):
+    """Miscellaneous manipulation of ticket workflow features."""
+
+    def prepare_ticket(req, ticket, fields, actions):
+        """Not currently called, but should be provided for future
+        compatibility."""
+
+    def validate_ticket(req, ticket):
+        """Validate a ticket after it's been populated from user input.
+        
+        Must return a list of `(field, message)` tuples, one for each problem
+        detected. `field` can be `None` to indicate an overall problem with the
+        ticket. Therefore, a return value of `[]` means everything is OK."""
+
+
+class TicketSystem(Component):
+    implements(IPermissionRequestor, IWikiSyntaxProvider, ISearchSource)
+
+    change_listeners = ExtensionPoint(ITicketChangeListener)
+
+    restrict_owner = BoolOption('ticket', 'restrict_owner', 'false',
+        """Make the owner field of tickets use a drop-down menu. See
+        [wiki:TracTickets#AssigntoasDropDownList AssignToAsDropDownList]
+        (''since 0.9'').""")
+
+    # Public API
+
+    def get_available_actions(self, ticket, perm_):
+        """Returns the actions that can be performed on the ticket."""
+        actions = {
+            'new':      ['leave', 'resolve', 'reassign', 'accept'],
+            'assigned': ['leave', 'resolve', 'reassign'          ],
+            'reopened': ['leave', 'resolve', 'reassign'          ],
+            'closed':   ['leave',                        'reopen']
+        }
+        perms = {'resolve': 'TICKET_MODIFY', 'reassign': 'TICKET_MODIFY',
+                 'accept': 'TICKET_MODIFY', 'reopen': 'TICKET_CREATE'}
+        return [action for action in actions.get(ticket['status'], ['leave'])
+                if action not in perms or perm_.has_permission(perms[action])]
+
+    def get_ticket_fields(self):
+        """Returns the list of fields available for tickets."""
+        from trac.ticket import model
+
+        db = self.env.get_db_cnx()
+        fields = []
+
+        # Basic text fields
+        for name in ('summary', 'reporter'):
+            field = {'name': name, 'type': 'text', 'label': name.title()}
+            fields.append(field)
+
+        # Owner field, can be text or drop-down depending on configuration
+        field = {'name': 'owner', 'label': 'Owner'}
+        if self.restrict_owner:
+            field['type'] = 'select'
+            users = []
+            perm = PermissionSystem(self.env)
+            for username, name, email in self.env.get_known_users(db):
+                if perm.get_user_permissions(username).get('TICKET_MODIFY'):
+                    users.append(username)
+            field['options'] = users
+            field['optional'] = True
+        else:
+            field['type'] = 'text'
+        fields.append(field)
+
+        # Description
+        fields.append({'name': 'description', 'type': 'textarea',
+                       'label': 'Description'})
+
+        # Default select and radio fields
+        selects = [('type', model.Type), ('status', model.Status),
+                   ('priority', model.Priority), ('milestone', model.Milestone),
+                   ('component', model.Component), ('version', model.Version),
+                   ('severity', model.Severity), ('resolution', model.Resolution)]
+        for name, cls in selects:
+            options = [val.name for val in cls.select(self.env, db=db)]
+            if not options:
+                # Fields without possible values are treated as if they didn't
+                # exist
+                continue
+            field = {'name': name, 'type': 'select', 'label': name.title(),
+                     'value': self.config.get('ticket', 'default_' + name),
+                     'options': options}
+            if name in ('status', 'resolution'):
+                field['type'] = 'radio'
+            elif name in ('milestone', 'version'):
+                field['optional'] = True
+            fields.append(field)
+
+        # Advanced text fields
+        for name in ('keywords', 'cc', ):
+            field = {'name': name, 'type': 'text', 'label': name.title()}
+            fields.append(field)
+
+        for field in self.get_custom_fields():
+            if field['name'] in [f['name'] for f in fields]:
+                self.log.warning('Duplicate field name "%s" (ignoring)',
+                                 field['name'])
+                continue
+            if not re.match('^[a-zA-Z][a-zA-Z0-9_]+$', field['name']):
+                self.log.warning('Invalid name for custom field: "%s" '
+                                 '(ignoring)', field['name'])
+                continue
+            field['custom'] = True
+            fields.append(field)
+
+        return fields
+
+    def get_custom_fields(self):
+        fields = []
+        config = self.config['ticket-custom']
+        for name in [option for option, value in config.options()
+                     if '.' not in option]:
+            field = {
+                'name': name,
+                'type': config.get(name),
+                'order': config.getint(name + '.order', 0),
+                'label': config.get(name + '.label') or name.capitalize(),
+                'value': config.get(name + '.value', '')
+            }
+            if field['type'] == 'select' or field['type'] == 'radio':
+                field['options'] = config.getlist(name + '.options', sep='|')
+            elif field['type'] == 'textarea':
+                field['width'] = config.getint(name + '.cols')
+                field['height'] = config.getint(name + '.rows')
+            fields.append(field)
+
+        fields.sort(lambda x, y: cmp(x['order'], y['order']))
+        return fields
+
+    # IPermissionRequestor methods
+
+    def get_permission_actions(self):
+        return ['TICKET_APPEND', 'TICKET_CREATE', 'TICKET_CHGPROP',
+                'TICKET_VIEW',
+                ('TICKET_MODIFY', ['TICKET_APPEND', 'TICKET_CHGPROP']),  
+                ('TICKET_ADMIN', ['TICKET_CREATE', 'TICKET_MODIFY',  
+                                  'TICKET_VIEW'])]
+
+    # IWikiSyntaxProvider methods
+
+    def get_link_resolvers(self):
+        return [('bug', self._format_link),
+                ('ticket', self._format_link),
+                ('comment', self._format_comment_link)]
+
+    def get_wiki_syntax(self):
+        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, fullmatch=None):
+        intertrac = formatter.shorthand_intertrac_helper(ns, target, label,
+                                                         fullmatch)
+        if intertrac:
+            return intertrac
+        try:
+            cursor = formatter.db.cursor()
+            cursor.execute("SELECT summary,status FROM ticket WHERE id=%s",
+                           (str(int(target)),))
+            row = cursor.fetchone()
+            if row:
+                return html.A(label, class_='%s ticket' % row[1],
+                              title=shorten_line(row[0]) + ' (%s)' % row[1],
+                              href=formatter.href.ticket(target))
+        except ValueError:
+            pass
+        return html.A(label, class_='missing ticket', rel='nofollow',
+                      href=formatter.href.ticket(target))
+
+    def _format_comment_link(self, formatter, ns, target, label):
+        type, id, cnum = 'ticket', '1', 0
+        href = None
+        if ':' in target:
+            elts = target.split(':')
+            if len(elts) == 3:
+                type, id, cnum = elts
+                href = formatter.href(type, id)
+        else:
+            # FIXME: the formatter should know which object the text being
+            #        formatted belongs to
+            if formatter.req:
+                path_info = formatter.req.path_info.strip('/').split('/', 2)
+                if len(path_info) == 2:
+                    type, id = path_info[:2]
+                    href = formatter.href(type, id)
+                    cnum = target
+        if href:
+            return html.A(label, href="%s#comment:%s" % (href, cnum),
+                          title="Comment %s for %s:%s" % (cnum, type, id))
+        else:
+            return label
+ 
+    # ISearchSource methods
+
+    def get_search_filters(self, req):
+        if req.perm.has_permission('TICKET_VIEW'):
+            yield ('ticket', 'Tickets')
+
+    def get_search_results(self, req, terms, filters):
+        if not 'ticket' in filters:
+            return
+        db = self.env.get_db_cnx()
+        sql, args = search_to_sql(db, ['b.newvalue'], terms)
+        sql2, args2 = search_to_sql(db, ['summary', 'keywords', 'description',
+                                         'reporter', 'cc'], terms)
+        cursor = db.cursor()
+        cursor.execute("SELECT DISTINCT a.summary,a.description,a.reporter, "
+                       "a.keywords,a.id,a.time,a.status FROM ticket a "
+                       "LEFT JOIN ticket_change b ON a.id = b.ticket "
+                       "WHERE (b.field='comment' AND %s ) OR %s" % (sql, sql2),
+                       args + args2)
+        for summary, desc, author, keywords, tid, date, status in cursor:
+            ticket = '#%d: ' % tid
+            if status == 'closed':
+                ticket = Markup('<span style="text-decoration: line-through">'
+                                '#%s</span>: ', tid)
+            yield (req.href.ticket(tid),
+                   ticket + shorten_line(summary),
+                   date, author, shorten_result(desc, terms))
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/model.py
@@ -0,0 +1,765 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2006 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+import time
+import sys
+import re
+
+from trac.core import TracError
+from trac.ticket import TicketSystem
+from trac.util import sorted, embedded_numbers
+
+__all__ = ['Ticket', 'Type', 'Status', 'Resolution', 'Priority', 'Severity',
+           'Component', 'Milestone', 'Version']
+
+
+class Ticket(object):
+
+    def __init__(self, env, tkt_id=None, db=None):
+        self.env = env
+        self.fields = TicketSystem(self.env).get_ticket_fields()
+        self.values = {}
+        if tkt_id:
+            self._fetch_ticket(tkt_id, db)
+        else:
+            self._init_defaults(db)
+            self.id = self.time_created = self.time_changed = None
+        self._old = {}
+
+    def _get_db(self, db):
+        return db or self.env.get_db_cnx()
+
+    def _get_db_for_write(self, db):
+        if db:
+            return (db, False)
+        else:
+            return (self.env.get_db_cnx(), True)
+
+    exists = property(fget=lambda self: self.id is not None)
+
+    def _init_defaults(self, db=None):
+        for field in self.fields:
+            default = None
+            if not field.get('custom'):
+                default = self.env.config.get('ticket',
+                                              'default_' + field['name'])
+            else:
+                default = field.get('value')
+                options = field.get('options')
+                if default and options and default not in options:
+                    try:
+                        default_idx = int(default)
+                        if default_idx > len(options):
+                            raise ValueError
+                        default = options[default_idx]
+                    except ValueError:
+                        self.env.log.warning('Invalid default value for '
+                                             'custom field "%s"'
+                                             % field['name'])
+            if default:
+                self.values.setdefault(field['name'], default)
+
+    def _fetch_ticket(self, tkt_id, db=None):
+        db = self._get_db(db)
+
+        # Fetch the standard ticket fields
+        std_fields = [f['name'] for f in self.fields if not f.get('custom')]
+        cursor = db.cursor()
+        cursor.execute("SELECT %s,time,changetime FROM ticket WHERE id=%%s"
+                       % ','.join(std_fields), (tkt_id,))
+        row = cursor.fetchone()
+        if not row:
+            raise TracError('Ticket %d does not exist.' % tkt_id,
+                            'Invalid Ticket Number')
+
+        self.id = tkt_id
+        for i in range(len(std_fields)):
+            self.values[std_fields[i]] = row[i] or ''
+        self.time_created = row[len(std_fields)]
+        self.time_changed = row[len(std_fields) + 1]
+
+        # Fetch custom fields if available
+        custom_fields = [f['name'] for f in self.fields if f.get('custom')]
+        cursor.execute("SELECT name,value FROM ticket_custom WHERE ticket=%s",
+                       (tkt_id,))
+        for name, value in cursor:
+            if name in custom_fields:
+                self.values[name] = value
+
+    def __getitem__(self, name):
+        return self.values[name]
+
+    def __setitem__(self, name, value):
+        """Log ticket modifications so the table ticket_change can be updated"""
+        if self.values.has_key(name) and self.values[name] == value:
+            return
+        if not self._old.has_key(name): # Changed field
+            self._old[name] = self.values.get(name)
+        elif self._old[name] == value: # Change of field reverted
+            del self._old[name]
+        if value:
+            field = [field for field in self.fields if field['name'] == name]
+            if field and field[0].get('type') != 'textarea':
+                value = value.strip()
+        self.values[name] = value
+
+    def populate(self, values):
+        """Populate the ticket with 'suitable' values from a dictionary"""
+        field_names = [f['name'] for f in self.fields]
+        for name in [name for name in values.keys() if name in field_names]:
+            self[name] = values.get(name, '')
+
+        # We have to do an extra trick to catch unchecked checkboxes
+        for name in [name for name in values.keys() if name[9:] in field_names
+                     and name.startswith('checkbox_')]:
+            if not values.has_key(name[9:]):
+                self[name[9:]] = '0'
+
+    def insert(self, when=0, db=None):
+        """Add ticket to database"""
+        assert not self.exists, 'Cannot insert an existing ticket'
+        db, handle_ta = self._get_db_for_write(db)
+
+        # Add a timestamp
+        if not when:
+            when = int(time.time())
+        self.time_created = self.time_changed = when
+
+        cursor = db.cursor()
+
+        # The owner field defaults to the component owner
+        if self.values.get('component') and not self.values.get('owner'):
+            try:
+                component = Component(self.env, self['component'], db=db)
+                if component.owner:
+                    self['owner'] = component.owner
+            except TracError, e:
+                # Assume that no such component exists
+                pass
+
+        # Insert ticket record
+        std_fields = [f['name'] for f in self.fields if not f.get('custom')
+                      and self.values.has_key(f['name'])]
+        cursor.execute("INSERT INTO ticket (%s,time,changetime) VALUES (%s)"
+                       % (','.join(std_fields),
+                          ','.join(['%s'] * (len(std_fields) + 2))),
+                       [self[name] for name in std_fields] +
+                       [self.time_created, self.time_changed])
+        tkt_id = db.get_last_id(cursor, 'ticket')
+
+        # Insert custom fields
+        custom_fields = [f['name'] for f in self.fields if f.get('custom')
+                         and self.values.has_key(f['name'])]
+        if custom_fields:
+            cursor.executemany("INSERT INTO ticket_custom (ticket,name,value) "
+                               "VALUES (%s,%s,%s)", [(tkt_id, name, self[name])
+                                                     for name in custom_fields])
+
+        if handle_ta:
+            db.commit()
+
+        self.id = tkt_id
+        self._old = {}
+
+        for listener in TicketSystem(self.env).change_listeners:
+            listener.ticket_created(self)
+
+        return self.id
+
+    def save_changes(self, author, comment, when=0, db=None, cnum=''):
+        """
+        Store ticket changes in the database. The ticket must already exist in
+        the database.
+        """
+        assert self.exists, 'Cannot update a new ticket'
+
+        if not self._old and not comment:
+            return # Not modified
+
+        db, handle_ta = self._get_db_for_write(db)
+        cursor = db.cursor()
+        when = int(when or time.time())
+
+        if self.values.has_key('component'):
+            # If the component is changed on a 'new' ticket then owner field
+            # is updated accordingly. (#623).
+            if self.values.get('status') == 'new' \
+                    and self._old.has_key('component') \
+                    and not self._old.has_key('owner'):
+                try:
+                    old_comp = Component(self.env, self._old['component'], db)
+                    old_owner = old_comp.owner or ''
+                    current_owner = self.values.get('owner') or ''
+                    if old_owner == current_owner:
+                        new_comp = Component(self.env, self['component'], db)
+                        self['owner'] = new_comp.owner
+                except TracError, e:
+                    # If the old component has been removed from the database we
+                    # just leave the owner as is.
+                    pass
+
+        # Fix up cc list separators and remove duplicates
+        if self.values.has_key('cc'):
+            cclist = []
+            for cc in re.split(r'[;,\s]+', self.values['cc']):
+                if cc not in cclist:
+                    cclist.append(cc)
+            self.values['cc'] = ', '.join(cclist)
+
+        custom_fields = [f['name'] for f in self.fields if f.get('custom')]
+        for name in self._old.keys():
+            if name in custom_fields:
+                cursor.execute("SELECT * FROM ticket_custom " 
+                               "WHERE ticket=%s and name=%s", (self.id, name))
+                if cursor.fetchone():
+                    cursor.execute("UPDATE ticket_custom SET value=%s "
+                                   "WHERE ticket=%s AND name=%s",
+                                   (self[name], self.id, name))
+                else:
+                    cursor.execute("INSERT INTO ticket_custom (ticket,name,"
+                                   "value) VALUES(%s,%s,%s)",
+                                   (self.id, name, self[name]))
+            else:
+                cursor.execute("UPDATE ticket SET %s=%%s WHERE id=%%s" % name,
+                               (self[name], self.id))
+            cursor.execute("INSERT INTO ticket_change "
+                           "(ticket,time,author,field,oldvalue,newvalue) "
+                           "VALUES (%s, %s, %s, %s, %s, %s)",
+                           (self.id, when, author, name, self._old[name],
+                            self[name]))
+        # always save comment, even if empty (numbering support for timeline)
+        cursor.execute("INSERT INTO ticket_change "
+                       "(ticket,time,author,field,oldvalue,newvalue) "
+                       "VALUES (%s,%s,%s,'comment',%s,%s)",
+                       (self.id, when, author, cnum, comment))
+
+        cursor.execute("UPDATE ticket SET changetime=%s WHERE id=%s",
+                       (when, self.id))
+
+        if handle_ta:
+            db.commit()
+        self._old = {}
+        self.time_changed = when
+
+        for listener in TicketSystem(self.env).change_listeners:
+            listener.ticket_changed(self, comment, self._old)
+
+    def get_changelog(self, when=0, db=None):
+        """Return the changelog as a list of tuples of the form
+        (time, author, field, oldvalue, newvalue, permanent).
+
+        While the other tuple elements are quite self-explanatory,
+        the `permanent` flag is used to distinguish collateral changes
+        that are not yet immutable (like attachments, currently).
+        """
+        db = self._get_db(db)
+        cursor = db.cursor()
+        if when:
+            cursor.execute("SELECT time,author,field,oldvalue,newvalue,1 "
+                           "FROM ticket_change WHERE ticket=%s AND time=%s "
+                           "UNION "
+                           "SELECT time,author,'attachment',null,filename,0 "
+                           "FROM attachment WHERE id=%s AND time=%s "
+                           "UNION "
+                           "SELECT time,author,'comment',null,description,0 "
+                           "FROM attachment WHERE id=%s AND time=%s "
+                           "ORDER BY time",
+                           (self.id, when, str(self.id), when, self.id, when))
+        else:
+            cursor.execute("SELECT time,author,field,oldvalue,newvalue,1 "
+                           "FROM ticket_change WHERE ticket=%s "
+                           "UNION "
+                           "SELECT time,author,'attachment',null,filename,0 "
+                           "FROM attachment WHERE id=%s "
+                           "UNION "
+                           "SELECT time,author,'comment',null,description,0 "
+                           "FROM attachment WHERE id=%s "
+                           "ORDER BY time", (self.id,  str(self.id), self.id))
+        log = []
+        for t, author, field, oldvalue, newvalue, permanent in cursor:
+            log.append((int(t), author, field, oldvalue or '', newvalue or '',
+                        permanent))
+        return log
+
+    def delete(self, db=None):
+        db, handle_ta = self._get_db_for_write(db)
+        cursor = db.cursor()
+        cursor.execute("DELETE FROM ticket WHERE id=%s", (self.id,))
+        cursor.execute("DELETE FROM ticket_change WHERE ticket=%s", (self.id,))
+        cursor.execute("DELETE FROM attachment "
+                       " WHERE type='ticket' and id=%s", (self.id,))
+        cursor.execute("DELETE FROM ticket_custom WHERE ticket=%s", (self.id,))
+
+        if handle_ta:
+            db.commit()
+
+        for listener in TicketSystem(self.env).change_listeners:
+            listener.ticket_deleted(self)
+
+
+class AbstractEnum(object):
+    type = None
+    ticket_col = None
+
+    def __init__(self, env, name=None, db=None):
+        if not self.ticket_col:
+            self.ticket_col = self.type
+        self.env = env
+        if name:
+            if not db:
+                db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            cursor.execute("SELECT value FROM enum WHERE type=%s AND name=%s",
+                           (self.type, name))
+            row = cursor.fetchone()
+            if not row:
+                raise TracError, '%s %s does not exist.' % (self.type, name)
+            self.value = self._old_value = row[0]
+            self.name = self._old_name = name
+        else:
+            self.value = self._old_value = None
+            self.name = self._old_name = None
+
+    exists = property(fget=lambda self: self._old_value is not None)
+
+    def delete(self, db=None):
+        assert self.exists, 'Cannot deleting non-existent %s' % self.type
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Deleting %s %s' % (self.type, self.name))
+        cursor.execute("DELETE FROM enum WHERE type=%s AND value=%s",
+                       (self.type, self._old_value))
+
+        if handle_ta:
+            db.commit()
+        self.value = self._old_value = None
+        self.name = self._old_name = None
+
+    def insert(self, db=None):
+        assert not self.exists, 'Cannot insert existing %s' % self.type
+        assert self.name, 'Cannot create %s with no name' % self.type
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.debug("Creating new %s '%s'" % (self.type, self.name))
+        if not self.value:
+            cursor.execute(("SELECT COALESCE(MAX(%s),0) FROM enum "
+                            "WHERE type=%%s") % db.cast('value', 'int'),
+                           (self.type,))
+            self.value = int(float(cursor.fetchone()[0])) + 1
+        cursor.execute("INSERT INTO enum (type,name,value) VALUES (%s,%s,%s)",
+                       (self.type, self.name, self.value))
+
+        if handle_ta:
+            db.commit()
+        self._old_name = self.name
+        self._old_value = self.value
+
+    def update(self, db=None):
+        assert self.exists, 'Cannot update non-existent %s' % self.type
+        assert self.name, 'Cannot update %s with no name' % self.type
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Updating %s "%s"' % (self.type, self.name))
+        cursor.execute("UPDATE enum SET name=%s,value=%s "
+                       "WHERE type=%s AND name=%s",
+                       (self.name, self.value, self.type, self._old_name))
+        if self.name != self._old_name:
+            # Update tickets
+            cursor.execute("UPDATE ticket SET %s=%%s WHERE %s=%%s" %
+                           (self.ticket_col, self.ticket_col),
+                           (self.name, self._old_name))
+
+        if handle_ta:
+            db.commit()
+        self._old_name = self.name
+        self._old_value = self.value
+
+    def select(cls, env, db=None):
+        if not db:
+            db = env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT name,value FROM enum WHERE type=%s "
+                       "ORDER BY value", (cls.type,))
+        for name, value in cursor:
+            obj = cls(env)
+            obj.name = obj._old_name = name
+            obj.value = obj._old_value = value
+            yield obj
+    select = classmethod(select)
+
+
+class Type(AbstractEnum):
+    type = 'ticket_type'
+    ticket_col = 'type'
+
+
+class Status(AbstractEnum):
+    type = 'status'
+
+
+class Resolution(AbstractEnum):
+    type = 'resolution'
+
+
+class Priority(AbstractEnum):
+    type = 'priority'
+
+
+class Severity(AbstractEnum):
+    type = 'severity'
+
+
+class Component(object):
+
+    def __init__(self, env, name=None, db=None):
+        self.env = env
+        if name:
+            if not db:
+                db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            cursor.execute("SELECT owner,description FROM component "
+                           "WHERE name=%s", (name,))
+            row = cursor.fetchone()
+            if not row:
+                raise TracError, 'Component %s does not exist.' % name
+            self.name = self._old_name = name
+            self.owner = row[0] or None
+            self.description = row[1] or ''
+        else:
+            self.name = self._old_name = None
+            self.owner = None
+            self.description = None
+
+    exists = property(fget=lambda self: self._old_name is not None)
+
+    def delete(self, db=None):
+        assert self.exists, 'Cannot deleting non-existent component'
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Deleting component %s' % self.name)
+        cursor.execute("DELETE FROM component WHERE name=%s", (self.name,))
+
+        self.name = self._old_name = None
+
+        if handle_ta:
+            db.commit()
+
+    def insert(self, db=None):
+        assert not self.exists, 'Cannot insert existing component'
+        assert self.name, 'Cannot create component with no name'
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.debug("Creating new component '%s'" % self.name)
+        cursor.execute("INSERT INTO component (name,owner,description) "
+                       "VALUES (%s,%s,%s)",
+                       (self.name, self.owner, self.description))
+
+        if handle_ta:
+            db.commit()
+
+    def update(self, db=None):
+        assert self.exists, 'Cannot update non-existent component'
+        assert self.name, 'Cannot update component with no name'
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Updating component "%s"' % self.name)
+        cursor.execute("UPDATE component SET name=%s,owner=%s,description=%s "
+                       "WHERE name=%s",
+                       (self.name, self.owner, self.description,
+                        self._old_name))
+        if self.name != self._old_name:
+            # Update tickets
+            cursor.execute("UPDATE ticket SET component=%s WHERE component=%s",
+                           (self.name, self._old_name))
+            self._old_name = self.name
+
+        if handle_ta:
+            db.commit()
+
+    def select(cls, env, db=None):
+        if not db:
+            db = env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT name,owner,description FROM component "
+                       "ORDER BY name")
+        for name, owner, description in cursor:
+            component = cls(env)
+            component.name = name
+            component.owner = owner or None
+            component.description = description or ''
+            yield component
+    select = classmethod(select)
+
+
+class Milestone(object):
+
+    def __init__(self, env, name=None, db=None):
+        self.env = env
+        if name:
+            self._fetch(name, db)
+            self._old_name = name
+        else:
+            self.name = self._old_name = None
+            self.due = self.completed = 0
+            self.description = ''
+
+    def _fetch(self, name, db=None):
+        if not db:
+            db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT name,due,completed,description "
+                       "FROM milestone WHERE name=%s", (name,))
+        row = cursor.fetchone()
+        if not row:
+            raise TracError('Milestone %s does not exist.' % name,
+                            'Invalid Milestone Name')
+        self.name = row[0]
+        self.due = row[1] and int(row[1]) or 0
+        self.completed = row[2] and int(row[2]) or 0
+        self.description = row[3] or ''
+
+    exists = property(fget=lambda self: self._old_name is not None)
+    is_completed = property(fget=lambda self: self.completed != 0)
+    is_late = property(fget=lambda self: self.due and \
+                                         self.due < time.time() - 86400)
+
+    def delete(self, retarget_to=None, author=None, db=None):
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Deleting milestone %s' % self.name)
+        cursor.execute("DELETE FROM milestone WHERE name=%s", (self.name,))
+
+        # Retarget/reset tickets associated with this milestone
+        now = time.time()
+        cursor.execute("SELECT id FROM ticket WHERE milestone=%s", (self.name,))
+        tkt_ids = [int(row[0]) for row in cursor]
+        for tkt_id in tkt_ids:
+            ticket = Ticket(self.env, tkt_id, db)
+            ticket['milestone'] = retarget_to
+            ticket.save_changes(author, 'Milestone %s deleted' % self.name,
+                                now, db=db)
+
+        if handle_ta:
+            db.commit()
+
+    def insert(self, db=None):
+        assert self.name, 'Cannot create milestone with no name'
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.debug("Creating new milestone '%s'" % self.name)
+        cursor.execute("INSERT INTO milestone (name,due,completed,description) "
+                       "VALUES (%s,%s,%s,%s)",
+                       (self.name, self.due, self.completed, self.description))
+
+        if handle_ta:
+            db.commit()
+
+    def update(self, db=None):
+        assert self.name, 'Cannot update milestone with no name'
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Updating milestone "%s"' % self.name)
+        cursor.execute("UPDATE milestone SET name=%s,due=%s,"
+                       "completed=%s,description=%s WHERE name=%s",
+                       (self.name, self.due, self.completed, self.description,
+                        self._old_name))
+        self.env.log.info('Updating milestone field of all tickets '
+                          'associated with milestone "%s"' % self.name)
+        cursor.execute("UPDATE ticket SET milestone=%s WHERE milestone=%s",
+                       (self.name, self._old_name))
+        self._old_name = self.name
+
+        if handle_ta:
+            db.commit()
+
+    def select(cls, env, include_completed=True, db=None):
+        if not db:
+            db = env.get_db_cnx()
+        sql = "SELECT name,due,completed,description FROM milestone "
+        if not include_completed:
+            sql += "WHERE COALESCE(completed,0)=0 "
+        cursor = db.cursor()
+        cursor.execute(sql)
+        milestones = []
+        for name,due,completed,description in cursor:
+            milestone = Milestone(env)
+            milestone.name = milestone._old_name = name
+            milestone.due = due and int(due) or 0
+            milestone.completed = completed and int(completed) or 0
+            milestone.description = description or ''
+            milestones.append(milestone)
+        def milestone_order(m):
+            return (m.completed or sys.maxint,
+                    m.due or sys.maxint,
+                    embedded_numbers(m.name))
+        return sorted(milestones, key=milestone_order)
+    select = classmethod(select)
+
+
+class Version(object):
+
+    def __init__(self, env, name=None, db=None):
+        self.env = env
+        if name:
+            if not db:
+                db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            cursor.execute("SELECT time,description FROM version "
+                           "WHERE name=%s", (name,))
+            row = cursor.fetchone()
+            if not row:
+                raise TracError, 'Version %s does not exist.' % name
+            self.name = self._old_name = name
+            self.time = row[0] and int(row[0]) or None
+            self.description = row[1] or ''
+        else:
+            self.name = self._old_name = None
+            self.time = None
+            self.description = None
+
+    exists = property(fget=lambda self: self._old_name is not None)
+
+    def delete(self, db=None):
+        assert self.exists, 'Cannot deleting non-existent version'
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Deleting version %s' % self.name)
+        cursor.execute("DELETE FROM version WHERE name=%s", (self.name,))
+
+        self.name = self._old_name = None
+
+        if handle_ta:
+            db.commit()
+
+    def insert(self, db=None):
+        assert not self.exists, 'Cannot insert existing version'
+        assert self.name, 'Cannot create version with no name'
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.debug("Creating new version '%s'" % self.name)
+        cursor.execute("INSERT INTO version (name,time,description) "
+                       "VALUES (%s,%s,%s)",
+                       (self.name, self.time, self.description))
+
+        if handle_ta:
+            db.commit()
+
+    def update(self, db=None):
+        assert self.exists, 'Cannot update non-existent version'
+        assert self.name, 'Cannot update version with no name'
+        self.name = self.name.strip()
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        cursor = db.cursor()
+        self.env.log.info('Updating version "%s"' % self.name)
+        cursor.execute("UPDATE version SET name=%s,time=%s,description=%s "
+                       "WHERE name=%s",
+                       (self.name, self.time, self.description,
+                        self._old_name))
+        if self.name != self._old_name:
+            # Update tickets
+            cursor.execute("UPDATE ticket SET version=%s WHERE version=%s",
+                           (self.name, self._old_name))
+            self._old_name = self.name
+
+        if handle_ta:
+            db.commit()
+
+    def select(cls, env, db=None):
+        if not db:
+            db = env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT name,time,description FROM version")
+        versions = []
+        for name, time, description in cursor:
+            version = cls(env)
+            version.name = name
+            version.time = time and int(time) or None
+            version.description = description or ''
+            versions.append(version)
+        def version_order(v):
+            return (v.time or sys.maxint, embedded_numbers(v.name))
+        return sorted(versions, key=version_order, reverse=True)
+    select = classmethod(select)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/notification.py
@@ -0,0 +1,255 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Daniel Lundin <daniel@edgewall.com>
+# Copyright (C) 2005-2006 Emmanuel Blot <emmanuel.blot@free.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Daniel Lundin <daniel@edgewall.com>
+#
+
+import md5
+
+from trac import __version__
+from trac.core import *
+from trac.config import *
+from trac.util.text import CRLF, wrap
+from trac.notification import NotifyEmail
+
+
+class TicketNotificationSystem(Component):
+
+    always_notify_owner = BoolOption('notification', 'always_notify_owner',
+                                     'false',
+        """Always send notifications to the ticket owner (''since 0.9'').""")
+
+    always_notify_reporter = BoolOption('notification', 'always_notify_reporter',
+                                        'false',
+        """Always send notifications to any address in the ''reporter''
+        field.""")
+
+    always_notify_updater = BoolOption('notification', 'always_notify_updater',
+                                       'true',
+        """Always send notifications to the person who causes the ticket 
+        property change.""")
+
+
+class TicketNotifyEmail(NotifyEmail):
+    """Notification of ticket changes."""
+
+    template_name = "ticket_notify_email.cs"
+    ticket = None
+    newticket = None
+    modtime = 0
+    from_email = 'trac+ticket@localhost'
+    COLS = 75
+
+    def __init__(self, env):
+        NotifyEmail.__init__(self, env)
+        self.prev_cc = []
+
+    def notify(self, ticket, req, newticket=True, modtime=0):
+        self.ticket = ticket
+        self.modtime = modtime
+        self.newticket = newticket
+        self.ticket['description'] = wrap(self.ticket.values.get('description', ''),
+                                          self.COLS, initial_indent=' ',
+                                          subsequent_indent=' ', linesep=CRLF)
+        self.hdf.set_unescaped('email.ticket_props', self.format_props())
+        self.hdf.set_unescaped('email.ticket_body_hdr', self.format_hdr())
+        self.hdf['ticket.new'] = self.newticket
+        subject = self.format_subj()
+        link = req.abs_href.ticket(ticket.id)
+        if not self.newticket:
+            subject = 'Re: ' + subject
+        self.hdf.set_unescaped('email.subject', subject)
+        changes = ''
+        if not self.newticket and modtime:  # Ticket change
+            from trac.ticket.web_ui import TicketModule
+            for change in TicketModule(self.env).grouped_changelog_entries(
+                ticket, self.db, when=modtime):
+                if not change['permanent']: # attachment with same time...
+                    continue
+                self.hdf.set_unescaped('ticket.change.author', 
+                                       change['author'])
+                self.hdf.set_unescaped('ticket.change.comment',
+                                       wrap(change['comment'], self.COLS,
+                                            ' ', ' ', CRLF))
+                link += '#comment:%d' % change['cnum']
+                for field, values in change['fields'].iteritems():
+                    old = values['old']
+                    new = values['new']
+                    pfx = 'ticket.change.%s' % field
+                    newv = ''
+                    if field == 'description':
+                        new_descr = wrap(new, self.COLS, ' ', ' ', CRLF)
+                        old_descr = wrap(old, self.COLS, '> ', '> ', CRLF)
+                        old_descr = old_descr.replace(2*CRLF, CRLF + '>' + CRLF)
+                        cdescr = CRLF
+                        cdescr += 'Old description:' + 2*CRLF + old_descr + 2*CRLF
+                        cdescr += 'New description:' + 2*CRLF + new_descr + CRLF
+                        self.hdf.set_unescaped('email.changes_descr', cdescr)
+                    elif field == 'cc':
+                        (addcc, delcc) = self.diff_cc(old, new)
+                        chgcc = ''
+                        if delcc:
+                            chgcc += wrap(" * cc: %s (removed)" % ', '.join(delcc), 
+                                          self.COLS, ' ', ' ', CRLF)
+                            chgcc += CRLF
+                        if addcc:
+                            chgcc += wrap(" * cc: %s (added)" % ', '.join(addcc), 
+                                          self.COLS, ' ', ' ', CRLF)
+                            chgcc += CRLF
+                        if chgcc:
+                            changes += chgcc
+                        self.prev_cc += old and self.parse_cc(old) or []
+                    else:
+                        newv = new
+                        l = 7 + len(field)
+                        chg = wrap('%s => %s' % (old, new), self.COLS - l, '',
+                                   l * ' ', CRLF)
+                        changes += '  * %s:  %s%s' % (field, chg, CRLF)
+                    if newv:
+                        self.hdf.set_unescaped('%s.oldvalue' % pfx, old)
+                        self.hdf.set_unescaped('%s.newvalue' % pfx, newv)
+            if changes:
+                self.hdf.set_unescaped('email.changes_body', changes)
+        self.ticket['link'] = link
+        self.hdf.set_unescaped('ticket', self.ticket.values)
+        NotifyEmail.notify(self, ticket.id, subject)
+
+    def format_props(self):
+        tkt = self.ticket
+        fields = [f for f in tkt.fields if f['name'] not in ('summary', 'cc')]
+        width = [0, 0, 0, 0]
+        i = 0
+        for f in [f['name'] for f in fields if f['type'] != 'textarea']:
+            if not tkt.values.has_key(f):
+                continue
+            fval = tkt[f]
+            if fval.find('\n') != -1:
+                continue
+            idx = 2 * (i % 2)
+            if len(f) > width[idx]:
+                width[idx] = len(f)
+            if len(fval) > width[idx + 1]:
+                width[idx + 1] = len(fval)
+            i += 1
+        format = ('%%%is:  %%-%is  |  ' % (width[0], width[1]),
+                  ' %%%is:  %%-%is%s' % (width[2], width[3], CRLF))
+        l = (width[0] + width[1] + 5)
+        sep = l * '-' + '+' + (self.COLS - l) * '-'
+        txt = sep + CRLF
+        big = []
+        i = 0
+        for f in [f for f in fields if f['name'] != 'description']:
+            fname = f['name']
+            if not tkt.values.has_key(fname):
+                continue
+            fval = tkt[fname]
+            if f['type'] == 'textarea' or '\n' in unicode(fval):
+                big.append((fname.capitalize(), CRLF.join(fval.splitlines())))
+            else:
+                txt += format[i % 2] % (fname.capitalize(), fval)
+                i += 1
+        if i % 2:
+            txt += CRLF
+        if big:
+            txt += sep
+            for name, value in big:
+                txt += CRLF.join(['', name + ':', value, '', ''])
+        txt += sep
+        return txt
+
+    def parse_cc(self, txt):
+        return filter(lambda x: '@' in x, txt.replace(',', ' ').split())
+
+    def diff_cc(self, old, new):
+        oldcc = NotifyEmail.addrsep_re.split(old)
+        newcc = NotifyEmail.addrsep_re.split(new)
+        added = [x for x in newcc if x and x not in oldcc]
+        removed = [x for x in oldcc if x and x not in newcc]
+        return (added, removed)
+
+    def format_hdr(self):
+        return '#%s: %s' % (self.ticket.id, wrap(self.ticket['summary'],
+                                                 self.COLS, linesep=CRLF))
+
+    def format_subj(self):
+        projname = self.config.get('project', 'name')
+        return '[%s] #%s: %s' % (projname, self.ticket.id,
+                                 self.ticket['summary'])
+
+    def get_recipients(self, tktid):
+        notify_reporter = self.config.getbool('notification',
+                                              'always_notify_reporter')
+        notify_owner = self.config.getbool('notification',
+                                           'always_notify_owner')
+        notify_updater = self.config.getbool('notification', 
+                                             'always_notify_updater')
+
+        ccrecipients = self.prev_cc
+        torecipients = []
+        cursor = self.db.cursor()
+
+        # Harvest email addresses from the cc, reporter, and owner fields
+        cursor.execute("SELECT cc,reporter,owner FROM ticket WHERE id=%s",
+                       (tktid,))
+        row = cursor.fetchone()
+        if row:
+            ccrecipients += row[0] and row[0].replace(',', ' ').split() or []
+            if notify_reporter:
+                torecipients.append(row[1])
+            if notify_owner:
+                torecipients.append(row[2])
+
+        # Harvest email addresses from the author field of ticket_change(s)
+        if notify_reporter:
+            cursor.execute("SELECT DISTINCT author,ticket FROM ticket_change "
+                           "WHERE ticket=%s", (tktid,))
+            for author,ticket in cursor:
+                torecipients.append(author)
+
+        # Suppress the updater from the recipients
+        if not notify_updater:
+            cursor.execute("SELECT author FROM ticket_change WHERE ticket=%s "
+                           "ORDER BY time DESC LIMIT 1", (tktid,))
+            (updater, ) = cursor.fetchone() 
+            torecipients = [r for r in torecipients if r and r != updater]
+
+        return (torecipients, ccrecipients)
+
+    def get_message_id(self, rcpt, modtime=0):
+        """Generate a predictable, but sufficiently unique message ID."""
+        s = '%s.%08d.%d.%s' % (self.config.get('project', 'url'),
+                               int(self.ticket.id), modtime, rcpt)
+        dig = md5.new(s).hexdigest()
+        host = self.from_email[self.from_email.find('@') + 1:]
+        msgid = '<%03d.%s@%s>' % (len(s), dig, host)
+        return msgid
+
+    def send(self, torcpts, ccrcpts):
+        hdrs = {}
+        always_cc = self.config['notification'].get('smtp_always_cc')
+        always_bcc = self.config['notification'].get('smtp_always_bcc')
+        dest = filter(None, torcpts) or filter(None, ccrcpts) or \
+               filter(None, [always_cc]) or filter(None, [always_bcc])
+        if not dest:
+            self.env.log.info('no recipient for a ticket notification')
+            return
+        hdrs['Message-ID'] = self.get_message_id(dest[0], self.modtime)
+        hdrs['X-Trac-Ticket-ID'] = str(self.ticket.id)
+        hdrs['X-Trac-Ticket-URL'] = self.ticket['link']
+        if not self.newticket:
+            hdrs['In-Reply-To'] = self.get_message_id(dest[0])
+            hdrs['References'] = self.get_message_id(dest[0])
+        NotifyEmail.send(self, torcpts, ccrcpts, hdrs)
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/query.py
@@ -0,0 +1,723 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2006 Edgewall Software
+# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+import re
+from StringIO import StringIO
+import time
+
+from trac.core import *
+from trac.db import get_column_names
+from trac.perm import IPermissionRequestor
+from trac.ticket import Ticket, TicketSystem
+from trac.util.datefmt import format_datetime, http_date
+from trac.util.text import shorten_line, CRLF
+from trac.util.markup import escape, html, unescape
+from trac.web import IRequestHandler
+from trac.web.chrome import add_link, add_script, add_stylesheet, \
+                            INavigationContributor
+from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider
+from trac.wiki.macros import WikiMacroBase
+from trac.mimeview.api import Mimeview, IContentConverter
+
+class QuerySyntaxError(Exception):
+    """Exception raised when a ticket query cannot be parsed from a string."""
+
+
+class Query(object):
+
+    def __init__(self, env, constraints=None, order=None, desc=0, group=None,
+                 groupdesc = 0, verbose=0):
+        self.env = env
+        self.constraints = constraints or {}
+        self.order = order
+        self.desc = desc
+        self.group = group
+        self.groupdesc = groupdesc
+        self.verbose = verbose
+        self.fields = TicketSystem(self.env).get_ticket_fields()
+        self.cols = [] # lazily initialized
+
+        if self.order != 'id' \
+                and self.order not in [f['name'] for f in self.fields]:
+            # order by priority by default
+            self.order = 'priority'
+
+        if self.group not in [f['name'] for f in self.fields]:
+            self.group = None
+
+    def from_string(cls, env, string, **kw):
+        filters = string.split('&')
+        kw_strs = ['order', 'group']
+        kw_bools = ['desc', 'groupdesc', 'verbose']
+        constraints = {}
+        for filter in filters:
+            filter = filter.split('=')
+            if len(filter) != 2:
+                raise QuerySyntaxError, 'Query filter requires field and ' \
+                                        'constraints separated by a "="'
+            field,values = filter
+            if not field:
+                raise QuerySyntaxError, 'Query filter requires field name'
+            values = values.split('|')
+            mode, neg = '', ''
+            if field[-1] in ('~', '^', '$'):
+                mode = field[-1]
+                field = field[:-1]
+            if field[-1] == '!':
+                neg = '!'
+                field = field[:-1]
+            values = map(lambda x: neg + mode + x, values)
+            try:
+                field = str(field)
+                if field in kw_strs:
+                    kw[field] = values[0]
+                elif field in kw_bools:
+                    kw[field] = True
+                else:
+                    constraints[field] = values
+            except UnicodeError:
+                pass # field must be a str, see `get_href()`
+        return cls(env, constraints, **kw)
+    from_string = classmethod(from_string)
+
+    def get_columns(self):
+        if self.cols:
+            return self.cols
+
+        # FIXME: the user should be able to configure which columns should
+        # be displayed
+        cols = ['id']
+        cols += [f['name'] for f in self.fields if f['type'] != 'textarea']
+        for col in ('reporter', 'keywords', 'cc'):
+            if col in cols:
+                cols.remove(col)
+                cols.append(col)
+
+        # Semi-intelligently remove columns that are restricted to a single
+        # value by a query constraint.
+        for col in [k for k in self.constraints.keys() if k in cols]:
+            constraint = self.constraints[col]
+            if len(constraint) == 1 and constraint[0] \
+                    and not constraint[0][0] in ('!', '~', '^', '$'):
+                if col in cols:
+                    cols.remove(col)
+            if col == 'status' and not 'closed' in constraint \
+                    and 'resolution' in cols:
+                cols.remove('resolution')
+        if self.group in cols:
+            cols.remove(self.group)
+
+        def sort_columns(col1, col2):
+            constrained_fields = self.constraints.keys()
+            # Ticket ID is always the first column
+            if 'id' in [col1, col2]:
+                return col1 == 'id' and -1 or 1
+            # Ticket summary is always the second column
+            elif 'summary' in [col1, col2]:
+                return col1 == 'summary' and -1 or 1
+            # Constrained columns appear before other columns
+            elif col1 in constrained_fields or col2 in constrained_fields:
+                return col1 in constrained_fields and -1 or 1
+            return 0
+        cols.sort(sort_columns)
+
+        # Only display the first eight columns by default
+        # FIXME: Make this configurable on a per-user and/or per-query basis
+        self.cols = cols[:7]
+        if not self.order in self.cols and not self.order == self.group:
+            # Make sure the column we order by is visible, if it isn't also
+            # the column we group by
+            self.cols[-1] = self.order
+
+        return self.cols
+
+    def execute(self, req, db=None):
+        if not self.cols:
+            self.get_columns()
+
+        sql, args = self.get_sql()
+        self.env.log.debug("Query SQL: " + sql % tuple([repr(a) for a in args]))
+
+        if not db:
+            db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute(sql, args)
+        columns = get_column_names(cursor)
+        results = []
+        for row in cursor:
+            id = int(row[0])
+            result = {'id': id, 'href': req.href.ticket(id)}
+            for i in range(1, len(columns)):
+                name, val = columns[i], row[i]
+                if name == self.group:
+                    val = val or 'None'
+                elif name == 'reporter':
+                    val = val or 'anonymous'
+                elif name in ['changetime', 'time']:
+                    val = int(val)
+                elif val is None:
+                    val = '--'
+                result[name] = val
+            results.append(result)
+        cursor.close()
+        return results
+
+    def get_href(self, req, order=None, desc=None, format=None):
+        # FIXME: only use .href from that 'req' for now
+        if desc is None:
+            desc = self.desc
+        if order is None:
+            order = self.order
+        return req.href.query(order=order, desc=desc and 1 or None,
+                              group=self.group or None,
+                              groupdesc=self.groupdesc and 1 or None,
+                              verbose=self.verbose and 1 or None,
+                              format=format, **self.constraints)
+
+    def get_sql(self):
+        """Return a (sql, params) tuple for the query."""
+        if not self.cols:
+            self.get_columns()
+
+        # Build the list of actual columns to query
+        cols = self.cols[:]
+        def add_cols(*args):
+            for col in args:
+                if not col in cols:
+                    cols.append(col)
+        if self.group and not self.group in cols:
+            add_cols(self.group)
+        if self.verbose:
+            add_cols('reporter', 'description')
+        add_cols('priority', 'time', 'changetime', self.order)
+        cols.extend([c for c in self.constraints.keys() if not c in cols])
+
+        custom_fields = [f['name'] for f in self.fields if f.has_key('custom')]
+
+        sql = []
+        sql.append("SELECT " + ",".join(['t.%s AS %s' % (c, c) for c in cols
+                                         if c not in custom_fields]))
+        sql.append(",priority.value AS priority_value")
+        for k in [k for k in cols if k in custom_fields]:
+            sql.append(",%s.value AS %s" % (k, k))
+        sql.append("\nFROM ticket AS t")
+
+        # Join with ticket_custom table as necessary
+        for k in [k for k in cols if k in custom_fields]:
+           sql.append("\n  LEFT OUTER JOIN ticket_custom AS %s ON " \
+                      "(id=%s.ticket AND %s.name='%s')" % (k, k, k, k))
+
+        # Join with the enum table for proper sorting
+        for col in [c for c in ('status', 'resolution', 'priority', 'severity')
+                    if c == self.order or c == self.group or c == 'priority']:
+            sql.append("\n  LEFT OUTER JOIN enum AS %s ON "
+                       "(%s.type='%s' AND %s.name=%s)"
+                       % (col, col, col, col, col))
+
+        # Join with the version/milestone tables for proper sorting
+        for col in [c for c in ['milestone', 'version']
+                    if c == self.order or c == self.group]:
+            sql.append("\n  LEFT OUTER JOIN %s ON (%s.name=%s)"
+                       % (col, col, col))
+
+        def get_constraint_sql(name, value, mode, neg):
+            if name not in custom_fields:
+                name = 't.' + name
+            else:
+                name = name + '.value'
+            value = value[len(mode) + neg:]
+
+            if mode == '':
+                return ("COALESCE(%s,'')%s=%%s" % (name, neg and '!' or ''),
+                        value)
+            if not value:
+                return None
+
+            if mode == '~':
+                value = '%' + value + '%'
+            elif mode == '^':
+                value = value + '%'
+            elif mode == '$':
+                value = '%' + value
+            return ("COALESCE(%s,'') %sLIKE %%s" % (name, neg and 'NOT ' or ''),
+                    value)
+
+        clauses = []
+        args = []
+        for k, v in self.constraints.items():
+            # Determine the match mode of the constraint (contains, starts-with,
+            # negation, etc)
+            neg = v[0].startswith('!')
+            mode = ''
+            if len(v[0]) > neg and v[0][neg] in ('~', '^', '$'):
+                mode = v[0][neg]
+
+            # Special case for exact matches on multiple values
+            if not mode and len(v) > 1:
+                if k not in custom_fields:
+                    col = 't.' + k
+                else:
+                    col = k + '.value'
+                clauses.append("COALESCE(%s,'') %sIN (%s)"
+                               % (col, neg and 'NOT ' or '',
+                                  ','.join(['%s' for val in v])))
+                args += [val[neg:] for val in v]
+            elif len(v) > 1:
+                constraint_sql = filter(None,
+                                        [get_constraint_sql(k, val, mode, neg)
+                                         for val in v])
+                if not constraint_sql:
+                    continue
+                if neg:
+                    clauses.append("(" + " AND ".join([item[0] for item in constraint_sql]) + ")")
+                else:
+                    clauses.append("(" + " OR ".join([item[0] for item in constraint_sql]) + ")")
+                args += [item[1] for item in constraint_sql]
+            elif len(v) == 1:
+                constraint_sql = get_constraint_sql(k, v[0], mode, neg)
+                if constraint_sql:
+                    clauses.append(constraint_sql[0])
+                    args.append(constraint_sql[1])
+
+        clauses = filter(None, clauses)
+        if clauses:
+            sql.append("\nWHERE " + " AND ".join(clauses))
+
+        sql.append("\nORDER BY ")
+        order_cols = [(self.order, self.desc)]
+        if self.group and self.group != self.order:
+            order_cols.insert(0, (self.group, self.groupdesc))
+        for name, desc in order_cols:
+            if name not in custom_fields:
+                col = 't.' + name
+            else:
+                col = name + '.value'
+            if name == 'id':
+                # FIXME: This is a somewhat ugly hack.  Can we also have the
+                #        column type for this?  If it's an integer, we do first
+                #        one, if text, we do 'else'
+                if desc:
+                    sql.append("COALESCE(%s,0)=0 DESC," % col)
+                else:
+                    sql.append("COALESCE(%s,0)=0," % col)
+            else:
+                if desc:
+                    sql.append("COALESCE(%s,'')='' DESC," % col)
+                else:
+                    sql.append("COALESCE(%s,'')=''," % col)
+            if name in ['status', 'resolution', 'priority', 'severity']:
+                if desc:
+                    sql.append("%s.value DESC" % name)
+                else:
+                    sql.append("%s.value" % name)
+            elif col in ['t.milestone', 't.version']:
+                time_col = name == 'milestone' and 'milestone.due' or 'version.time'
+                if desc:
+                    sql.append("COALESCE(%s,0)=0 DESC,%s DESC,%s DESC"
+                               % (time_col, time_col, col))
+                else:
+                    sql.append("COALESCE(%s,0)=0,%s,%s"
+                               % (time_col, time_col, col))
+            else:
+                if desc:
+                    sql.append("%s DESC" % col)
+                else:
+                    sql.append("%s" % col)
+            if name == self.group and not name == self.order:
+                sql.append(",")
+        if self.order != 'id':
+            sql.append(",t.id")
+
+        return "".join(sql), args
+
+
+class QueryModule(Component):
+
+    implements(IRequestHandler, INavigationContributor, IWikiSyntaxProvider,
+               IContentConverter)
+
+    # IContentConverter methods
+    def get_supported_conversions(self):
+        yield ('rss', 'RSS Feed', 'xml',
+               'trac.ticket.Query', 'application/rss+xml', 8)
+        yield ('csv', 'Comma-delimited Text', 'csv',
+               'trac.ticket.Query', 'text/csv', 8)
+        yield ('tab', 'Tab-delimited Text', 'tsv',
+               'trac.ticket.Query', 'text/tab-separated-values', 8)
+
+    def convert_content(self, req, mimetype, query, key):
+        if key == 'rss':
+            return self.export_rss(req, query)
+        elif key == 'csv':
+            return self.export_csv(req, query, mimetype='text/csv')
+        elif key == 'tab':
+            return self.export_csv(req, query, '\t', 'text/tab-separated-values')
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'tickets'
+
+    def get_navigation_items(self, req):
+        from trac.ticket.report import ReportModule
+        if req.perm.has_permission('TICKET_VIEW') and \
+                not self.env.is_component_enabled(ReportModule):
+            yield ('mainnav', 'tickets',
+                   html.A('View Tickets', href=req.href.query()))
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        return req.path_info == '/query'
+
+    def process_request(self, req):
+        req.perm.assert_permission('TICKET_VIEW')
+
+        constraints = self._get_constraints(req)
+        if not constraints and not req.args.has_key('order'):
+            # avoid displaying all tickets when the query module is invoked
+            # with no parameters. Instead show only open tickets, possibly
+            # associated with the user
+            constraints = {'status': ('new', 'assigned', 'reopened')}
+            if req.authname and req.authname != 'anonymous':
+                constraints['owner'] = (req.authname,)
+            else:
+                email = req.session.get('email')
+                name = req.session.get('name')
+                if email or name:
+                    constraints['cc'] = ('~%s' % email or name,)
+
+        query = Query(self.env, constraints, req.args.get('order'),
+                      req.args.has_key('desc'), req.args.get('group'),
+                      req.args.has_key('groupdesc'),
+                      req.args.has_key('verbose'))
+
+        if req.args.has_key('update'):
+            # Reset session vars
+            for var in ('query_constraints', 'query_time', 'query_tickets'):
+                if req.session.has_key(var):
+                    del req.session[var]
+            req.redirect(query.get_href(req))
+
+        # Add registered converters
+        for conversion in Mimeview(self.env).get_supported_conversions(
+                                             'trac.ticket.Query'):
+            add_link(req, 'alternate',
+                     query.get_href(req, format=conversion[0]),
+                     conversion[1], conversion[3])
+
+        constraints = {}
+        for k, v in query.constraints.items():
+            constraint = {'values': [], 'mode': ''}
+            for val in v:
+                neg = val.startswith('!')
+                if neg:
+                    val = val[1:]
+                mode = ''
+                if val[:1] in ('~', '^', '$'):
+                    mode, val = val[:1], val[1:]
+                constraint['mode'] = (neg and '!' or '') + mode
+                constraint['values'].append(val)
+            constraints[k] = constraint
+        req.hdf['query.constraints'] = constraints
+
+        format = req.args.get('format')
+        if format:
+            Mimeview(self.env).send_converted(req, 'trac.ticket.Query', query,
+                                              format, 'query')
+
+        self.display_html(req, query)
+        return 'query.cs', None
+
+    # Internal methods
+
+    def _get_constraints(self, req):
+        constraints = {}
+        ticket_fields = [f['name'] for f in
+                         TicketSystem(self.env).get_ticket_fields()]
+
+        # For clients without JavaScript, we remove constraints here if
+        # requested
+        remove_constraints = {}
+        to_remove = [k[10:] for k in req.args.keys()
+                     if k.startswith('rm_filter_')]
+        if to_remove: # either empty or containing a single element
+            match = re.match(r'(\w+?)_(\d+)$', to_remove[0])
+            if match:
+                remove_constraints[match.group(1)] = int(match.group(2))
+            else:
+                remove_constraints[to_remove[0]] = -1
+
+        for field in [k for k in req.args.keys() if k in ticket_fields]:
+            vals = req.args[field]
+            if not isinstance(vals, (list, tuple)):
+                vals = [vals]
+            if vals:
+                mode = req.args.get(field + '_mode')
+                if mode:
+                    vals = map(lambda x: mode + x, vals)
+                if remove_constraints.has_key(field):
+                    idx = remove_constraints[field]
+                    if idx >= 0:
+                        del vals[idx]
+                        if not vals:
+                            continue
+                    else:
+                        continue
+                constraints[field] = vals
+
+        return constraints
+
+    def _get_constraint_modes(self):
+        modes = {}
+        modes['text'] = [
+            {'name': "contains", 'value': "~"},
+            {'name': "doesn't contain", 'value': "!~"},
+            {'name': "begins with", 'value': "^"},
+            {'name': "ends with", 'value': "$"},
+            {'name': "is", 'value': ""},
+            {'name': "is not", 'value': "!"}
+        ]
+        modes['select'] = [
+            {'name': "is", 'value': ""},
+            {'name': "is not", 'value': "!"}
+        ]
+        return modes
+
+    def display_html(self, req, query):
+        req.hdf['title'] = 'Custom Query'
+        add_stylesheet(req, 'common/css/report.css')
+        add_script(req, 'common/js/query.js')
+
+        db = self.env.get_db_cnx()
+
+        for field in query.fields:
+            if field['type'] == 'textarea':
+                continue
+            hdf = {}
+            hdf.update(field)
+            del hdf['name']
+            req.hdf['query.fields.' + field['name']] = hdf
+        req.hdf['query.modes'] = self._get_constraint_modes()
+
+        # For clients without JavaScript, we add a new constraint here if
+        # requested
+        if req.args.has_key('add'):
+            field = req.args.get('add_filter')
+            if field:
+                idx = 0
+                if query.constraints.has_key(field):
+                    idx = len(query.constraints[field])
+                req.hdf['query.constraints.%s.values.%d' % (field, idx)] = ''
+
+        cols = query.get_columns()
+        labels = dict([(f['name'], f['label']) for f in query.fields])
+        for idx, col in enumerate(cols):
+            req.hdf['query.headers.%d' % idx] = {
+                'name': col, 'label': labels.get(col, 'Ticket'),
+                'href': query.get_href(req, order=col,
+                                       desc=(col == query.order and
+                                             not query.desc))
+            }
+
+        href = req.href.query(group=query.group,
+                              groupdesc=query.groupdesc and 1 or None,
+                              verbose=query.verbose and 1 or None,
+                              **query.constraints)
+        req.hdf['query.order'] = query.order
+        req.hdf['query.href'] = href
+        if query.desc:
+            req.hdf['query.desc'] = True
+        if query.group:
+            req.hdf['query.group'] = query.group
+            if query.groupdesc:
+                req.hdf['query.groupdesc'] = True
+        if query.verbose:
+            req.hdf['query.verbose'] = True
+
+        tickets = query.execute(req, db)
+        req.hdf['query.num_matches'] = len(tickets)
+
+        # The most recent query is stored in the user session
+        orig_list = rest_list = None
+        orig_time = int(time.time())
+        query_constraints = unicode(query.constraints)
+        if query_constraints != req.session.get('query_constraints') \
+                or int(req.session.get('query_time', 0)) < orig_time - 3600:
+            # New or outdated query, (re-)initialize session vars
+            req.session['query_constraints'] = query_constraints
+            req.session['query_tickets'] = ' '.join([str(t['id']) for t in tickets])
+        else:
+            orig_list = [int(id) for id in req.session.get('query_tickets', '').split()]
+            rest_list = orig_list[:]
+            orig_time = int(req.session.get('query_time', 0))
+        req.session['query_href'] = query.get_href(req)
+        req.session['query_time'] = orig_time
+
+        # Find out which tickets originally in the query results no longer
+        # match the constraints
+        if rest_list:
+            for tid in [t['id'] for t in tickets if t['id'] in rest_list]:
+                rest_list.remove(tid)
+            for rest_id in rest_list:
+                try:
+                    ticket = Ticket(self.env, int(rest_id), db=db)
+                    data = {'id': ticket.id, 'time': ticket.time_created,
+                            'changetime': ticket.time_changed, 'removed': True,
+                            'href': req.href.ticket(ticket.id)}
+                    data.update(ticket.values)
+                except TracError, e:
+                    data = {'id': rest_id, 'time': 0, 'changetime': 0,
+                            'summary': html.EM(e)}
+                tickets.insert(orig_list.index(rest_id), data)
+
+        num_matches_group = {}
+        for ticket in tickets:
+            if orig_list:
+                # Mark tickets added or changed since the query was first
+                # executed
+                if int(ticket['time']) > orig_time:
+                    ticket['added'] = True
+                elif int(ticket['changetime']) > orig_time:
+                    ticket['changed'] = True
+            for field, value in ticket.items():
+                if field == query.group:
+                    num_matches_group[value] = num_matches_group.get(value, 0)+1
+                if field == 'time':
+                    ticket[field] = format_datetime(value)
+                elif field == 'description':
+                    ticket[field] = wiki_to_html(value or '', self.env, req, db)
+                else:
+                    ticket[field] = value
+
+        req.hdf['query.results'] = tickets
+        req.hdf['query.num_matches_group'] = num_matches_group
+        req.session['query_tickets'] = ' '.join([str(t['id']) for t in tickets])
+
+        # Kludge: only show link to available reports if the report module is
+        # actually enabled
+        from trac.ticket.report import ReportModule
+        if req.perm.has_permission('REPORT_VIEW') and \
+           self.env.is_component_enabled(ReportModule):
+            req.hdf['query.report_href'] = req.href.report()
+
+    def export_csv(self, req, query, sep=',', mimetype='text/plain'):
+        content = StringIO()
+        cols = query.get_columns()
+        content.write(sep.join([col for col in cols]) + CRLF)
+
+        results = query.execute(req, self.env.get_db_cnx())
+        for result in results:
+            content.write(sep.join([unicode(result[col]).replace(sep, '_')
+                                                        .replace('\n', ' ')
+                                                        .replace('\r', ' ')
+                                    for col in cols]) + CRLF)
+        return (content.getvalue(), '%s;charset=utf-8' % mimetype)
+
+    def export_rss(self, req, query):
+        query.verbose = True
+        db = self.env.get_db_cnx()
+        results = query.execute(req, db)
+        for result in results:
+            result['href'] = req.abs_href.ticket(result['id'])
+            if result['reporter'].find('@') == -1:
+                result['reporter'] = ''
+            if result['description']:
+                # unicode() cancels out the Markup() returned by wiki_to_html
+                descr = wiki_to_html(result['description'], self.env, req, db,
+                                     absurls=True)
+                result['description'] = unicode(descr)
+            if result['time']:
+                result['time'] = http_date(result['time'])
+        req.hdf['query.results'] = results
+        req.hdf['query.href'] = req.abs_href.query(group=query.group,
+                groupdesc=query.groupdesc and 1 or None,
+                verbose=query.verbose and 1 or None,
+                **query.constraints)
+        return (req.hdf.render('query_rss.cs'), 'application/rss+xml')
+
+    # IWikiSyntaxProvider methods
+    
+    def get_wiki_syntax(self):
+        return []
+    
+    def get_link_resolvers(self):
+        yield ('query', self._format_link)
+
+    def _format_link(self, formatter, ns, query, label):
+        if query[0] == '?':
+            return html.A(label, class_='query',
+                          href=formatter.href.query() + query.replace(' ', '+'))
+        else:
+            from trac.ticket.query import Query, QuerySyntaxError
+            try:
+                query = Query.from_string(formatter.env, query)
+                return html.A(label, href=query.get_href(formatter), # Hack
+                              class_='query')
+            except QuerySyntaxError, e:
+                return html.EM('[Error: %s]' % e, class_='error')
+
+
+class TicketQueryMacro(WikiMacroBase):
+    """Macro that lists tickets that match certain criteria.
+    
+    This macro accepts two parameters, the second of which is optional.
+    
+    The first parameter is the query itself, and uses the same syntax as for
+    {{{query:}}} wiki links. The second parameter determines how the list of
+    tickets is presented: the default presentation is to list the ticket ID next
+    to the summary, with each ticket on a separate line. If the second parameter
+    is given and set to '''compact''' then the tickets are presented as a
+    comma-separated list of ticket IDs.
+    """
+
+    def render_macro(self, req, name, content):
+        query_string = ''
+        compact = 0
+        argv = content.split(',')
+        if len(argv) > 0:
+            query_string = argv[0]
+            if len(argv) > 1:
+                if argv[1].strip().lower() == 'compact':
+                    compact = 1
+
+        buf = StringIO()
+
+        query = Query.from_string(self.env, query_string)
+        query.order = 'id'
+        tickets = query.execute(req)
+        if tickets:
+            if compact:
+                links = []
+                for ticket in tickets:
+                    href = req.href.ticket(int(ticket['id']))
+                    summary = escape(shorten_line(ticket['summary']))
+                    a = '<a class="%s ticket" href="%s" title="%s">#%s</a>' % \
+                        (ticket['status'], href, summary, ticket['id'])
+                    links.append(a)
+                buf.write(', '.join(links))
+            else:
+                buf.write('<dl class="wiki compact">')
+                for ticket in tickets:
+                    href = req.href.ticket(int(ticket['id']))
+                    dt = '<dt><a class="%s ticket" href="%s">#%s</a></dt>' % \
+                         (ticket['status'], href, ticket['id'])
+                    buf.write(dt)
+                    buf.write('<dd>%s</dd>' % (escape(ticket['summary'])))
+                buf.write('</dl>')
+
+        return buf.getvalue()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/report.py
@@ -0,0 +1,503 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2006 Christian Boos <cboos@neuf.fr>
+# Copyright (C) 2006 Matthew Good <trac@matt-good.net>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+
+import re
+from StringIO import StringIO
+import urllib
+
+from trac import util
+from trac.core import *
+from trac.db import get_column_names
+from trac.perm import IPermissionRequestor
+from trac.util import sorted
+from trac.util.datefmt import format_date, format_time, format_datetime, \
+                               http_date
+from trac.util.markup import html
+from trac.web import IRequestHandler
+from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
+from trac.wiki import wiki_to_html, IWikiSyntaxProvider, Formatter
+
+
+class ReportModule(Component):
+
+    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
+               IWikiSyntaxProvider)
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'tickets'
+
+    def get_navigation_items(self, req):
+        if not req.perm.has_permission('REPORT_VIEW'):
+            return
+        yield ('mainnav', 'tickets',
+               html.A('View Tickets', href=req.href.report()))
+
+    # IPermissionRequestor methods  
+
+    def get_permission_actions(self):  
+        actions = ['REPORT_CREATE', 'REPORT_DELETE', 'REPORT_MODIFY',  
+                   'REPORT_SQL_VIEW', 'REPORT_VIEW']  
+        return actions + [('REPORT_ADMIN', actions)]  
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        match = re.match(r'/report(?:/([0-9]+))?', req.path_info)
+        if match:
+            if match.group(1):
+                req.args['id'] = match.group(1)
+            return True
+
+    def process_request(self, req):
+        req.perm.assert_permission('REPORT_VIEW')
+
+        # did the user ask for any special report?
+        id = int(req.args.get('id', -1))
+        action = req.args.get('action', 'list')
+
+        db = self.env.get_db_cnx()
+
+        if req.method == 'POST':
+            if action == 'new':
+                self._do_create(req, db)
+            elif action == 'delete':
+                self._do_delete(req, db, id)
+            elif action == 'edit':
+                self._do_save(req, db, id)
+        elif action in ('copy', 'edit', 'new'):
+            self._render_editor(req, db, id, action == 'copy')
+        elif action == 'delete':
+            self._render_confirm_delete(req, db, id)
+        else:
+            resp = self._render_view(req, db, id)
+            if not resp:
+               return None
+            template, content_type = resp
+            if content_type:
+               return resp
+
+        if id != -1 or action == 'new':
+            add_link(req, 'up', req.href.report(), 'Available Reports')
+
+            # Kludge: Reset session vars created by query module so that the
+            # query navigation links on the ticket page don't confuse the user
+            for var in ('query_constraints', 'query_time', 'query_tickets'):
+                if req.session.has_key(var):
+                    del req.session[var]
+
+        # Kludge: only show link to custom query if the query module is actually
+        # enabled
+        from trac.ticket.query import QueryModule
+        if req.perm.has_permission('TICKET_VIEW') and \
+           self.env.is_component_enabled(QueryModule):
+            req.hdf['report.query_href'] = req.href.query()
+
+        add_stylesheet(req, 'common/css/report.css')
+        return 'report.cs', None
+
+    # Internal methods
+
+    def _do_create(self, req, db):
+        req.perm.assert_permission('REPORT_CREATE')
+
+        if req.args.has_key('cancel'):
+            req.redirect(req.href.report())
+
+        title = req.args.get('title', '')
+        query = req.args.get('query', '')
+        description = req.args.get('description', '')
+        cursor = db.cursor()
+        cursor.execute("INSERT INTO report (title,query,description) "
+                       "VALUES (%s,%s,%s)", (title, query, description))
+        id = db.get_last_id(cursor, 'report')
+        db.commit()
+        req.redirect(req.href.report(id))
+
+    def _do_delete(self, req, db, id):
+        req.perm.assert_permission('REPORT_DELETE')
+
+        if req.args.has_key('cancel'):
+            req.redirect(req.href.report(id))
+
+        cursor = db.cursor()
+        cursor.execute("DELETE FROM report WHERE id=%s", (id,))
+        db.commit()
+        req.redirect(req.href.report())
+
+    def _do_save(self, req, db, id):
+        """
+        Saves report changes to the database
+        """
+        req.perm.assert_permission('REPORT_MODIFY')
+
+        if not req.args.has_key('cancel'):
+            title = req.args.get('title', '')
+            query = req.args.get('query', '')
+            description = req.args.get('description', '')
+            cursor = db.cursor()
+            cursor.execute("UPDATE report SET title=%s,query=%s,description=%s "
+                           "WHERE id=%s", (title, query, description, id))
+            db.commit()
+        req.redirect(req.href.report(id))
+
+    def _render_confirm_delete(self, req, db, id):
+        req.perm.assert_permission('REPORT_DELETE')
+
+        cursor = db.cursor()
+        cursor.execute("SELECT title FROM report WHERE id = %s", (id,))
+        row = cursor.fetchone()
+        if not row:
+            raise TracError('Report %s does not exist.' % id,
+                            'Invalid Report Number')
+        req.hdf['title'] = 'Delete Report {%s} %s' % (id, row[0])
+        req.hdf['report'] = {
+            'id': id,
+            'mode': 'delete',
+            'title': row[0],
+            'href': req.href.report(id)
+        }
+
+    def _render_editor(self, req, db, id, copy=False):
+        if id == -1:
+            req.perm.assert_permission('REPORT_CREATE')
+            title = query = description = ''
+        else:
+            req.perm.assert_permission('REPORT_MODIFY')
+            cursor = db.cursor()
+            cursor.execute("SELECT title,description,query FROM report "
+                           "WHERE id=%s", (id,))
+            row = cursor.fetchone()
+            if not row:
+                raise TracError('Report %s does not exist.' % id,
+                                'Invalid Report Number')
+            title = row[0] or ''
+            description = row[1] or ''
+            query = row[2] or ''
+
+        if copy:
+            title += ' (copy)'
+
+        if copy or id == -1:
+            req.hdf['title'] = 'Create New Report'
+            req.hdf['report.href'] = req.href.report()
+            req.hdf['report.action'] = 'new'
+        else:
+            req.hdf['title'] = 'Edit Report {%d} %s' % (id, title)
+            req.hdf['report.href'] = req.href.report(id)
+            req.hdf['report.action'] = 'edit'
+
+        req.hdf['report.id'] = id
+        req.hdf['report.mode'] = 'edit'
+        req.hdf['report.title'] = title
+        req.hdf['report.sql'] = query
+        req.hdf['report.description'] = description
+
+    def _render_view(self, req, db, id):
+        """
+        uses a user specified sql query to extract some information
+        from the database and presents it as a html table.
+        """
+        actions = {'create': 'REPORT_CREATE', 'delete': 'REPORT_DELETE',
+                   'modify': 'REPORT_MODIFY'}
+        for action in [k for k,v in actions.items()
+                       if req.perm.has_permission(v)]:
+            req.hdf['report.can_' + action] = True
+        req.hdf['report.href'] = req.href.report(id)
+
+        try:
+            args = self.get_var_args(req)
+        except ValueError,e:
+            raise TracError, 'Report failed: %s' % e
+
+        title, description, sql = self.get_info(db, id, args)
+
+        format = req.args.get('format')
+        if format == 'sql':
+            self._render_sql(req, id, title, description, sql)
+            return
+
+        req.hdf['report.mode'] = 'list'
+        if id > 0:
+            title = '{%i} %s' % (id, title)
+        req.hdf['title'] = title
+        req.hdf['report.title'] = title
+        req.hdf['report.id'] = id
+        req.hdf['report.description'] = wiki_to_html(description, self.env, req)
+        if id != -1:
+            self.add_alternate_links(req, args)
+
+        try:
+            cols, rows = self.execute_report(req, db, id, sql, args)
+        except Exception, e:
+            req.hdf['report.message'] = 'Report execution failed: %s' % e
+            return 'report.cs', None
+
+        # Convert the header info to HDF-format
+        idx = 0
+        for col in cols:
+            title=col.capitalize()
+            prefix = 'report.headers.%d' % idx
+            req.hdf['%s.real' % prefix] = col[0]
+            if title.startswith('__') and title.endswith('__'):
+                continue
+            elif title[0] == '_' and title[-1] == '_':
+                title = title[1:-1].capitalize()
+                req.hdf[prefix + '.fullrow'] = 1
+            elif title[0] == '_':
+                continue
+            elif title[-1] == '_':
+                title = title[:-1]
+                req.hdf[prefix + '.breakrow'] = 1
+            req.hdf[prefix] = title
+            idx = idx + 1
+
+        if req.args.has_key('sort'):
+            sortCol = req.args.get('sort')
+            colIndex = None
+            hiddenCols = 0
+            for x in range(len(cols)):
+                colName = cols[x]
+                if colName == sortCol:
+                    colIndex = x
+                if colName.startswith('__') and colName.endswith('__'):
+                    hiddenCols += 1
+            if colIndex != None:
+                k = 'report.headers.%d.asc' % (colIndex - hiddenCols)
+                asc = req.args.get('asc', None)
+                if asc:
+                    asc = int(asc) # string '0' or '1' to int/boolean
+                else:
+                    asc = 1
+                req.hdf[k] = asc
+                def sortkey(row):
+                    val = row[colIndex]
+                    if isinstance(val, basestring):
+                        val = val.lower()
+                    return val
+                rows = sorted(rows, key=sortkey, reverse=(not asc))
+
+        # Get the email addresses of all known users
+        email_map = {}
+        for username, name, email in self.env.get_known_users():
+            if email:
+                email_map[username] = email
+
+        # Convert the rows and cells to HDF-format
+        row_idx = 0
+        for row in rows:
+            col_idx = 0
+            numrows = len(row)
+            for cell in row:
+                cell = unicode(cell)
+                column = cols[col_idx]
+                value = {}
+                # Special columns begin and end with '__'
+                if column.startswith('__') and column.endswith('__'):
+                    value['hidden'] = 1
+                elif (column[0] == '_' and column[-1] == '_'):
+                    value['fullrow'] = 1
+                    column = column[1:-1]
+                    req.hdf[prefix + '.breakrow'] = 1
+                elif column[-1] == '_':
+                    value['breakrow'] = 1
+                    value['breakafter'] = 1
+                    column = column[:-1]
+                elif column[0] == '_':
+                    value['hidehtml'] = 1
+                    column = column[1:]
+                if column in ('ticket', 'id', '_id', '#', 'summary'):
+                    id_cols = [idx for idx, col in enumerate(cols)
+                               if col in ('ticket', 'id', '_id')]
+                    if id_cols:
+                        id_val = row[id_cols[0]]
+                        value['ticket_href'] = req.href.ticket(id_val)
+                elif column == 'description':
+                    desc = wiki_to_html(cell, self.env, req, db,
+                                        absurls=(format == 'rss'))
+                    value['parsed'] = format == 'rss' and unicode(desc) or desc
+                elif column == 'reporter':
+                    if cell.find('@') != -1:
+                        value['rss'] = cell
+                    elif cell in email_map:
+                        value['rss'] = email_map[cell]
+                elif column == 'report':
+                    value['report_href'] = req.href.report(cell)
+                elif column in ('time', 'date','changetime', 'created', 'modified'):
+                    value['date'] = format_date(cell)
+                    value['time'] = format_time(cell)
+                    value['datetime'] = format_datetime(cell)
+                    value['gmt'] = http_date(cell)
+                prefix = 'report.items.%d.%s' % (row_idx, unicode(column))
+                req.hdf[prefix] = unicode(cell)
+                for key in value.keys():
+                    req.hdf[prefix + '.' + key] = value[key]
+
+                col_idx += 1
+            row_idx += 1
+        req.hdf['report.numrows'] = row_idx
+
+        if format == 'rss':
+            return 'report_rss.cs', 'application/rss+xml'
+        elif format == 'csv':
+            self._render_csv(req, cols, rows)
+            return None
+        elif format == 'tab':
+            self._render_csv(req, cols, rows, '\t')
+            return None
+
+        return 'report.cs', None
+
+    def add_alternate_links(self, req, args):
+        params = args
+        if req.args.has_key('sort'):
+            params['sort'] = req.args['sort']
+        if req.args.has_key('asc'):
+            params['asc'] = req.args['asc']
+        href = ''
+        if params:
+            href = '&' + urllib.urlencode(params)
+        add_link(req, 'alternate', '?format=rss' + href, 'RSS Feed',
+                 'application/rss+xml', 'rss')
+        add_link(req, 'alternate', '?format=csv' + href,
+                 'Comma-delimited Text', 'text/plain')
+        add_link(req, 'alternate', '?format=tab' + href,
+                 'Tab-delimited Text', 'text/plain')
+        if req.perm.has_permission('REPORT_SQL_VIEW'):
+            add_link(req, 'alternate', '?format=sql', 'SQL Query',
+                     'text/plain')
+
+    def execute_report(self, req, db, id, sql, args):
+        sql, args = self.sql_sub_vars(req, sql, args)
+        if not sql:
+            raise TracError('Report %s has no SQL query.' % id)
+        if sql.find('__group__') == -1:
+            req.hdf['report.sorting.enabled'] = 1
+
+        self.log.debug('Executing report with SQL "%s" (%s)', sql, args)
+
+        cursor = db.cursor()
+        cursor.execute(sql, args)
+
+        # FIXME: fetchall should probably not be used.
+        info = cursor.fetchall() or []
+        cols = get_column_names(cursor)
+
+        db.rollback()
+
+        return cols, info
+
+    def get_info(self, db, id, args):
+        if id == -1:
+            # If no particular report was requested, display
+            # a list of available reports instead
+            title = 'Available Reports'
+            sql = 'SELECT id AS report, title FROM report ORDER BY report'
+            description = 'This is a list of reports available.'
+        else:
+            cursor = db.cursor()
+            cursor.execute("SELECT title,query,description from report "
+                           "WHERE id=%s", (id,))
+            row = cursor.fetchone()
+            if not row:
+                raise TracError('Report %d does not exist.' % id,
+                                'Invalid Report Number')
+            title = row[0] or ''
+            sql = row[1]
+            description = row[2] or ''
+
+        return [title, description, sql]
+
+    def get_var_args(self, req):
+        report_args = {}
+        for arg in req.args.keys():
+            if not arg == arg.upper():
+                continue
+            report_args[arg] = req.args.get(arg)
+
+        # Set some default dynamic variables
+        if not report_args.has_key('USER'):
+            report_args['USER'] = req.authname
+
+        return report_args
+
+    def sql_sub_vars(self, req, sql, args):
+        values = []
+        def add_value(aname):
+            try:
+                arg = args[aname]
+            except KeyError:
+                raise TracError("Dynamic variable '$%s' not defined." \
+                                % aname)
+            req.hdf['report.var.' + aname] = arg
+            values.append(arg)
+
+        # simple parameter substitution
+        def repl(match):
+            add_value(match.group(1))
+            return '%s'
+
+        var_re = re.compile("'?[$]([A-Z]+)'?")
+        sql_io = StringIO()
+
+        # break SQL into literals and non-literals to handle replacing
+        # variables within them with query parameters
+        for expr in re.split("('(?:[^']|(?:''))*')", sql):
+            sql_io.write(var_re.sub(repl, expr))
+        return sql_io.getvalue(), values
+
+    def _render_csv(self, req, cols, rows, sep=','):
+        req.send_response(200)
+        req.send_header('Content-Type', 'text/plain;charset=utf-8')
+        req.end_headers()
+
+        req.write(sep.join(cols) + '\r\n')
+        for row in rows:
+            req.write(sep.join(
+                [unicode(c).replace(sep,"_")
+                 .replace('\n',' ').replace('\r',' ') for c in row]) + '\r\n')
+
+    def _render_sql(self, req, id, title, description, sql):
+        req.perm.assert_permission('REPORT_SQL_VIEW')
+        req.send_response(200)
+        req.send_header('Content-Type', 'text/plain;charset=utf-8')
+        req.end_headers()
+
+        req.write('-- ## %s: %s ## --\n\n' % (id, title))
+        if description:
+            req.write('-- %s\n\n' % '\n-- '.join(description.splitlines()))
+        req.write(sql)
+        
+    # IWikiSyntaxProvider methods
+    
+    def get_link_resolvers(self):
+        yield ('report', self._format_link)
+
+    def get_wiki_syntax(self):
+        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, fullmatch=None):
+        intertrac = formatter.shorthand_intertrac_helper(ns, target, label,
+                                                         fullmatch)
+        if intertrac:
+            return intertrac
+        report, args, fragment = formatter.split_link(target)
+        return html.A(label, href=formatter.href.report(report) + args,
+                      class_='report')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/roadmap.py
@@ -0,0 +1,522 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2006 Edgewall Software
+# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+import re
+from time import localtime, strftime, time
+
+from trac import __version__
+from trac.core import *
+from trac.perm import IPermissionRequestor
+from trac.util.datefmt import format_date, format_datetime, parse_date, \
+                               pretty_timedelta
+from trac.util.text import shorten_line, CRLF, to_unicode
+from trac.util.markup import html, unescape, Markup
+from trac.ticket import Milestone, Ticket, TicketSystem
+from trac.Timeline import ITimelineEventProvider
+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
+
+
+def get_tickets_for_milestone(env, db, milestone, field='component'):
+    cursor = db.cursor()
+    fields = TicketSystem(env).get_ticket_fields()
+    if field in [f['name'] for f in fields if not f.get('custom')]:
+        cursor.execute("SELECT id,status,%s FROM ticket WHERE milestone=%%s "
+                       "ORDER BY %s" % (field, field), (milestone,))
+    else:
+        cursor.execute("SELECT id,status,value FROM ticket LEFT OUTER "
+                       "JOIN ticket_custom ON (id=ticket AND name=%s) "
+                       "WHERE milestone=%s ORDER BY value", (field, milestone))
+    tickets = []
+    for tkt_id, status, fieldval in cursor:
+        tickets.append({'id': tkt_id, 'status': status, field: fieldval})
+    return tickets
+
+def get_query_links(req, milestone, grouped_by='component', group=None):
+    q = {}
+    if not group:
+        q['all_tickets'] = req.href.query(milestone=milestone)
+        q['active_tickets'] = req.href.query(
+            milestone=milestone, status=('new', 'assigned', 'reopened'))
+        q['closed_tickets'] = req.href.query(
+            milestone=milestone, status='closed')
+    else:
+        q['all_tickets'] = req.href.query(
+            {grouped_by: group}, milestone=milestone)
+        q['active_tickets'] = req.href.query(
+            {grouped_by: group}, milestone=milestone,
+            status=('new', 'assigned', 'reopened'))
+        q['closed_tickets'] = req.href.query(
+            {grouped_by: group}, milestone=milestone, status='closed')
+    return q
+
+def calc_ticket_stats(tickets):
+    total_cnt = len(tickets)
+    active = [ticket for ticket in tickets if ticket['status'] != 'closed']
+    active_cnt = len(active)
+    closed_cnt = total_cnt - active_cnt
+
+    percent_active, percent_closed = 0, 0
+    if total_cnt > 0:
+        percent_active = round(float(active_cnt) / float(total_cnt) * 100)
+        percent_closed = round(float(closed_cnt) / float(total_cnt) * 100)
+        if percent_active + percent_closed > 100:
+            percent_closed -= 1
+
+    return {
+        'total_tickets': total_cnt,
+        'active_tickets': active_cnt,
+        'percent_active': percent_active,
+        'closed_tickets': closed_cnt,
+        'percent_closed': percent_closed
+    }
+
+def milestone_to_hdf(env, db, req, milestone):
+    safe_name = None
+    if milestone.exists:
+        safe_name = milestone.name.replace('/', '%2F')
+    hdf = {'name': milestone.name,
+           'href': req.href.milestone(safe_name)}
+    if milestone.description:
+        hdf['description_source'] = milestone.description
+        hdf['description'] = wiki_to_html(milestone.description, env, req, db)
+    if milestone.due:
+        hdf['due'] = milestone.due
+        hdf['due_date'] = format_date(milestone.due)
+        hdf['due_delta'] = pretty_timedelta(milestone.due + 86400)
+        hdf['late'] = milestone.is_late
+    if milestone.completed:
+        hdf['completed'] = milestone.completed
+        hdf['completed_date'] = format_datetime(milestone.completed)
+        hdf['completed_delta'] = pretty_timedelta(milestone.completed)
+    return hdf
+
+def _get_groups(env, db, by='component'):
+    for field in TicketSystem(env).get_ticket_fields():
+        if field['name'] == by:
+            if field.has_key('options'):
+                return field['options']
+            else:
+                cursor = db.cursor()
+                cursor.execute("SELECT DISTINCT %s FROM ticket ORDER BY %s"
+                               % (by, by))
+                return [row[0] for row in cursor]
+    return []
+
+
+class RoadmapModule(Component):
+
+    implements(INavigationContributor, IPermissionRequestor, IRequestHandler)
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'roadmap'
+
+    def get_navigation_items(self, req):
+        if not req.perm.has_permission('ROADMAP_VIEW'):
+            return
+        yield ('mainnav', 'roadmap',
+               html.a('Roadmap', href=req.href.roadmap(), accesskey=3))
+
+    # IPermissionRequestor methods
+
+    def get_permission_actions(self):
+        return ['ROADMAP_VIEW']
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        return re.match(r'/roadmap/?', req.path_info) is not None
+
+    def process_request(self, req):
+        req.perm.assert_permission('ROADMAP_VIEW')
+        req.hdf['title'] = 'Roadmap'
+
+        showall = req.args.get('show') == 'all'
+        req.hdf['roadmap.showall'] = showall
+
+        db = self.env.get_db_cnx()
+        milestones = [milestone_to_hdf(self.env, db, req, m)
+                      for m in Milestone.select(self.env, showall, db)]
+        req.hdf['roadmap.milestones'] = milestones        
+
+        for idx, milestone in enumerate(milestones):
+            milestone_name = unescape(milestone['name']) # Kludge
+            prefix = 'roadmap.milestones.%d.' % idx
+            tickets = get_tickets_for_milestone(self.env, db, milestone_name,
+                                                'owner')
+            req.hdf[prefix + 'stats'] = calc_ticket_stats(tickets)
+            for k, v in get_query_links(req, milestone_name).items():
+                req.hdf[prefix + 'queries.' + k] = v
+            milestone['tickets'] = tickets # for the iCalendar view
+
+        if req.args.get('format') == 'ics':
+            self.render_ics(req, db, milestones)
+            return
+
+        add_stylesheet(req, 'common/css/roadmap.css')
+
+        # FIXME should use the 'webcal:' scheme, probably
+        username = None
+        if req.authname and req.authname != 'anonymous':
+            username = req.authname
+        icshref = req.href.roadmap(show=req.args.get('show'),
+                                        user=username, format='ics')
+        add_link(req, 'alternate', icshref, 'iCalendar', 'text/calendar', 'ics')
+
+        return 'roadmap.cs', None
+
+    # Internal methods
+
+    def render_ics(self, req, db, milestones):
+        req.send_response(200)
+        req.send_header('Content-Type', 'text/calendar;charset=utf-8')
+        req.end_headers()
+
+        from trac.ticket import Priority
+        priorities = {}
+        for priority in Priority.select(self.env):
+            priorities[priority.name] = float(priority.value)
+        def get_priority(ticket):
+            value = priorities.get(ticket['priority'])
+            if value:
+                return int(value * 9 / len(priorities))
+
+        def get_status(ticket):
+            status = ticket['status']
+            if status == 'new' or status == 'reopened' and not ticket['owner']:
+                return 'NEEDS-ACTION'
+            elif status == 'assigned' or status == 'reopened':
+                return 'IN-PROCESS'
+            elif status == 'closed':
+                if ticket['resolution'] == 'fixed': return 'COMPLETED'
+                else: return 'CANCELLED'
+            else: return ''
+
+        def write_prop(name, value, params={}):
+            text = ';'.join([name] + [k + '=' + v for k, v in params.items()]) \
+                 + ':' + '\\n'.join(re.split(r'[\r\n]+', value))
+            firstline = 1
+            while text:
+                if not firstline: text = ' ' + text
+                else: firstline = 0
+                req.write(text[:75] + CRLF)
+                text = text[75:]
+
+        def write_date(name, value, params={}):
+            params['VALUE'] = 'DATE'
+            write_prop(name, strftime('%Y%m%d', value), params)
+
+        def write_utctime(name, value, params={}):
+            write_prop(name, strftime('%Y%m%dT%H%M%SZ', value), params)
+
+        host = req.base_url[req.base_url.find('://') + 3:]
+        user = req.args.get('user', 'anonymous')
+
+        write_prop('BEGIN', 'VCALENDAR')
+        write_prop('VERSION', '2.0')
+        write_prop('PRODID', '-//Edgewall Software//NONSGML Trac %s//EN'
+                   % __version__)
+        write_prop('METHOD', 'PUBLISH')
+        write_prop('X-WR-CALNAME',
+                   self.config.get('project', 'name') + ' - Roadmap')
+        for milestone in milestones:
+            uid = '<%s/milestone/%s@%s>' % (req.base_path, milestone['name'],
+                                            host)
+            if milestone.has_key('due'):
+                write_prop('BEGIN', 'VEVENT')
+                write_prop('UID', uid)
+                write_date('DTSTAMP', localtime(milestone['due']))
+                write_date('DTSTART', localtime(milestone['due']))
+                write_prop('SUMMARY', 'Milestone %s' % milestone['name'])
+                write_prop('URL', req.base_url + '/milestone/' +
+                           milestone['name'])
+                if milestone.has_key('description_source'):
+                    write_prop('DESCRIPTION', milestone['description_source'])
+                write_prop('END', 'VEVENT')
+            for tkt_id in [ticket['id'] for ticket in milestone['tickets']
+                           if ticket['owner'] == user]:
+                ticket = Ticket(self.env, tkt_id)
+                write_prop('BEGIN', 'VTODO')
+                if milestone.has_key('due'):
+                    write_prop('RELATED-TO', uid)
+                    write_date('DUE', localtime(milestone['due']))
+                write_prop('SUMMARY', 'Ticket #%i: %s' % (ticket.id,
+                                                          ticket['summary']))
+                write_prop('URL', req.abs_href.ticket(ticket.id))
+                write_prop('DESCRIPTION', ticket['description'])
+                priority = get_priority(ticket)
+                if priority:
+                    write_prop('PRIORITY', unicode(priority))
+                write_prop('STATUS', get_status(ticket))
+                if ticket['status'] == 'closed':
+                    cursor = db.cursor()
+                    cursor.execute("SELECT time FROM ticket_change "
+                                   "WHERE ticket=%s AND field='status' "
+                                   "ORDER BY time desc LIMIT 1",
+                                   (ticket.id,))
+                    row = cursor.fetchone()
+                    if row:
+                        write_utctime('COMPLETED', localtime(row[0]))
+                write_prop('END', 'VTODO')
+        write_prop('END', 'VCALENDAR')
+
+
+class MilestoneModule(Component):
+
+    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
+               ITimelineEventProvider, IWikiSyntaxProvider)
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'roadmap'
+
+    def get_navigation_items(self, req):
+        return []
+
+    # IPermissionRequestor methods
+
+    def get_permission_actions(self):
+        actions = ['MILESTONE_CREATE', 'MILESTONE_DELETE', 'MILESTONE_MODIFY',
+                   'MILESTONE_VIEW']
+        return actions + [('MILESTONE_ADMIN', actions),
+                          ('ROADMAP_ADMIN', actions)]
+
+    # ITimelineEventProvider methods
+
+    def get_timeline_filters(self, req):
+        if req.perm.has_permission('MILESTONE_VIEW'):
+            yield ('milestone', 'Milestones')
+
+    def get_timeline_events(self, req, start, stop, filters):
+        if 'milestone' in filters:
+            format = req.args.get('format')
+            db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            cursor.execute("SELECT completed,name,description FROM milestone "
+                           "WHERE completed>=%s AND completed<=%s",
+                           (start, stop,))
+            for completed, name, description in cursor:
+                title = Markup('Milestone <em>%s</em> completed', name)
+                if format == 'rss':
+                    href = req.abs_href.milestone(name)
+                    message = wiki_to_html(description, self.env, req, db,
+                                           absurls=True)
+                else:
+                    href = req.href.milestone(name)
+                    message = wiki_to_oneliner(description, self.env, db,
+                                               shorten=True)
+                yield 'milestone', href, title, completed, None, message or '--'
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        import re, urllib
+        match = re.match(r'/milestone(?:/(.+))?', req.path_info)
+        if match:
+            if match.group(1):
+                req.args['id'] = match.group(1)
+            return True
+
+    def process_request(self, req):
+        req.perm.assert_permission('MILESTONE_VIEW')
+
+        add_link(req, 'up', req.href.roadmap(), 'Roadmap')
+
+        db = self.env.get_db_cnx()
+        milestone = Milestone(self.env, req.args.get('id'), db)
+        action = req.args.get('action', 'view')
+
+        if req.method == 'POST':
+            if req.args.has_key('cancel'):
+                if milestone.exists:
+                    safe_name = milestone.name.replace('/', '%2F')
+                    req.redirect(req.href.milestone(safe_name))
+                else:
+                    req.redirect(req.href.roadmap())
+            elif action == 'edit':
+                self._do_save(req, db, milestone)
+            elif action == 'delete':
+                self._do_delete(req, db, milestone)
+        elif action in ('new', 'edit'):
+            self._render_editor(req, db, milestone)
+        elif action == 'delete':
+            self._render_confirm(req, db, milestone)
+        else:
+            self._render_view(req, db, milestone)
+
+        add_stylesheet(req, 'common/css/roadmap.css')
+        return 'milestone.cs', None
+
+    # Internal methods
+
+    def _do_delete(self, req, db, milestone):
+        req.perm.assert_permission('MILESTONE_DELETE')
+
+        retarget_to = None
+        if req.args.has_key('retarget'):
+            retarget_to = req.args.get('target')
+        milestone.delete(retarget_to, req.authname)
+        db.commit()
+        req.redirect(req.href.roadmap())
+
+    def _do_save(self, req, db, milestone):
+        if milestone.exists:
+            req.perm.assert_permission('MILESTONE_MODIFY')
+        else:
+            req.perm.assert_permission('MILESTONE_CREATE')
+
+        if not req.args.has_key('name'):
+            raise TracError('You must provide a name for the milestone.',
+                            'Required Field Missing')
+        milestone.name = req.args.get('name')
+
+        due = req.args.get('duedate', '')
+        try:
+            milestone.due = due and parse_date(due) or 0
+        except ValueError, e:
+            raise TracError(to_unicode(e), 'Invalid Date Format')
+        if req.args.has_key('completed'):
+            completed = req.args.get('completeddate', '')
+            try:
+                milestone.completed = completed and parse_date(completed) or 0
+            except ValueError, e:
+                raise TracError(to_unicode(e), 'Invalid Date Format')
+            if milestone.completed > time():
+                raise TracError('Completion date may not be in the future',
+                                'Invalid Completion Date')
+            retarget_to = req.args.get('target')
+            if req.args.has_key('retarget'):
+                cursor = db.cursor()
+                cursor.execute("UPDATE ticket SET milestone=%s WHERE "
+                               "milestone=%s and status != 'closed'",
+                                (retarget_to, milestone.name))
+                self.env.log.info('Tickets associated with milestone %s '
+                                  'retargeted to %s' % 
+                                  (milestone.name, retarget_to))
+        else:
+            milestone.completed = 0
+
+        milestone.description = req.args.get('description', '')
+
+        if milestone.exists:
+            milestone.update()
+        else:
+            milestone.insert()
+        db.commit()
+
+        safe_name = milestone.name.replace('/', '%2F')
+        req.redirect(req.href.milestone(safe_name))
+
+    def _render_confirm(self, req, db, milestone):
+        req.perm.assert_permission('MILESTONE_DELETE')
+
+        req.hdf['title'] = 'Milestone %s' % milestone.name
+        req.hdf['milestone'] = milestone_to_hdf(self.env, db, req, milestone)
+        req.hdf['milestone.mode'] = 'delete'
+
+        for idx,other in enumerate(Milestone.select(self.env, False, db)):
+            if other.name == milestone.name:
+                continue
+            req.hdf['milestones.%d' % idx] = other.name
+
+    def _render_editor(self, req, db, milestone):
+        if milestone.exists:
+            req.perm.assert_permission('MILESTONE_MODIFY')
+            req.hdf['title'] = 'Milestone %s' % milestone.name
+            req.hdf['milestone.mode'] = 'edit'
+            req.hdf['milestones'] = [m.name for m in
+                                     Milestone.select(self.env)
+                                     if m.name != milestone.name]
+        else:
+            req.perm.assert_permission('MILESTONE_CREATE')
+            req.hdf['title'] = 'New Milestone'
+            req.hdf['milestone.mode'] = 'new'
+
+        from trac.util.datefmt import get_date_format_hint, \
+                                       get_datetime_format_hint
+        req.hdf['milestone'] = milestone_to_hdf(self.env, db, req, milestone)
+        req.hdf['milestone.date_hint'] = get_date_format_hint()
+        req.hdf['milestone.datetime_hint'] = get_datetime_format_hint()
+        req.hdf['milestone.datetime_now'] = format_datetime()
+
+    def _render_view(self, req, db, milestone):
+        req.hdf['title'] = 'Milestone %s' % milestone.name
+        req.hdf['milestone.mode'] = 'view'
+
+        req.hdf['milestone'] = milestone_to_hdf(self.env, db, req, milestone)
+
+        available_groups = []
+        component_group_available = False
+        for field in TicketSystem(self.env).get_ticket_fields():
+            if field['type'] == 'select' and field['name'] != 'milestone' \
+                    or field['name'] == 'owner':
+                available_groups.append({'name': field['name'],
+                                         'label': field['label']})
+                if field['name'] == 'component':
+                    component_group_available = True
+        req.hdf['milestone.stats.available_groups'] = available_groups
+
+        if component_group_available:
+            by = req.args.get('by', 'component')
+        else:
+            by = req.args.get('by', available_groups[0]['name'])
+        req.hdf['milestone.stats.grouped_by'] = by
+
+        tickets = get_tickets_for_milestone(self.env, db, milestone.name, by)
+        stats = calc_ticket_stats(tickets)
+        req.hdf['milestone.stats'] = stats
+        for key, value in get_query_links(req, milestone.name).items():
+            req.hdf['milestone.queries.' + key] = value
+
+        groups = _get_groups(self.env, db, by)
+        group_no = 0
+        max_percent_total = 0
+        for group in groups:
+            group_tickets = [t for t in tickets if t[by] == group]
+            if not group_tickets:
+                continue
+            prefix = 'milestone.stats.groups.%s' % group_no
+            req.hdf['%s.name' % prefix] = group
+            percent_total = 0
+            if len(tickets) > 0:
+                percent_total = float(len(group_tickets)) / float(len(tickets))
+                if percent_total > max_percent_total:
+                    max_percent_total = percent_total
+            req.hdf['%s.percent_total' % prefix] = percent_total * 100
+            stats = calc_ticket_stats(group_tickets)
+            req.hdf[prefix] = stats
+            for key, value in \
+                    get_query_links(req, milestone.name, by, group).items():
+                req.hdf['%s.queries.%s' % (prefix, key)] = value
+            group_no += 1
+        req.hdf['milestone.stats.max_percent_total'] = max_percent_total * 100
+
+    # IWikiSyntaxProvider methods
+
+    def get_wiki_syntax(self):
+        return []
+
+    def get_link_resolvers(self):
+        yield ('milestone', self._format_link)
+
+    def _format_link(self, formatter, ns, name, label):
+        return html.A(label, href=formatter.href.milestone(name),
+                      class_='milestone')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/tests/__init__.py
@@ -0,0 +1,17 @@
+import unittest
+
+from trac.ticket.tests import api, model, query, wikisyntax, notification, \
+                              conversion
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(api.suite())
+    suite.addTest(model.suite())
+    suite.addTest(query.suite())
+    suite.addTest(wikisyntax.suite())
+    suite.addTest(notification.suite())
+    suite.addTest(conversion.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/tests/api.py
@@ -0,0 +1,109 @@
+from trac.config import Configuration
+from trac.ticket.api import TicketSystem
+from trac.test import EnvironmentStub, Mock
+
+import unittest
+
+
+class TicketSystemTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.ticket_system = TicketSystem(self.env)
+
+    def test_custom_field_text(self):
+        self.env.config.set('ticket-custom', 'test', 'text')
+        self.env.config.set('ticket-custom', 'test.label', 'Test')
+        self.env.config.set('ticket-custom', 'test.value', 'Foo bar')
+        fields = TicketSystem(self.env).get_custom_fields()
+        self.assertEqual({'name': 'test', 'type': 'text', 'label': 'Test',
+                          'value': 'Foo bar', 'order': 0},
+                         fields[0])
+
+    def test_custom_field_select(self):
+        self.env.config.set('ticket-custom', 'test', 'select')
+        self.env.config.set('ticket-custom', 'test.label', 'Test')
+        self.env.config.set('ticket-custom', 'test.value', '1')
+        self.env.config.set('ticket-custom', 'test.options', 'option1|option2')
+        fields = TicketSystem(self.env).get_custom_fields()
+        self.assertEqual({'name': 'test', 'type': 'select', 'label': 'Test',
+                          'value': '1', 'options': ['option1', 'option2'],
+                          'order': 0},
+                         fields[0])
+
+    def test_custom_field_textarea(self):
+        self.env.config.set('ticket-custom', 'test', 'textarea')
+        self.env.config.set('ticket-custom', 'test.label', 'Test')
+        self.env.config.set('ticket-custom', 'test.value', 'Foo bar')
+        self.env.config.set('ticket-custom', 'test.cols', '60')
+        self.env.config.set('ticket-custom', 'test.rows', '4')
+        fields = TicketSystem(self.env).get_custom_fields()
+        self.assertEqual({'name': 'test', 'type': 'textarea', 'label': 'Test',
+                          'value': 'Foo bar', 'width': 60, 'height': 4,
+                          'order': 0},
+                         fields[0])
+
+    def test_custom_field_order(self):
+        self.env.config.set('ticket-custom', 'test1', 'text')
+        self.env.config.set('ticket-custom', 'test1.order', '2')
+        self.env.config.set('ticket-custom', 'test2', 'text')
+        self.env.config.set('ticket-custom', 'test2.order', '1')
+        fields = TicketSystem(self.env).get_custom_fields()
+        self.assertEqual('test2', fields[0]['name'])
+        self.assertEqual('test1', fields[1]['name'])
+
+    def test_available_actions_full_perms(self):
+        ts = TicketSystem(self.env)
+        perm = Mock(has_permission=lambda x: 1)
+        self.assertEqual(['leave', 'resolve', 'reassign', 'accept'],
+                         ts.get_available_actions({'status': 'new'}, perm))
+        self.assertEqual(['leave', 'resolve', 'reassign'],
+                         ts.get_available_actions({'status': 'assigned'}, perm))
+        self.assertEqual(['leave', 'resolve', 'reassign'],
+                         ts.get_available_actions({'status': 'reopened'}, perm))
+        self.assertEqual(['leave', 'reopen'],
+                         ts.get_available_actions({'status': 'closed'}, perm))
+
+    def test_available_actions_no_perms(self):
+        ts = TicketSystem(self.env)
+        perm = Mock(has_permission=lambda x: 0)
+        self.assertEqual(['leave'],
+                         ts.get_available_actions({'status': 'new'}, perm))
+        self.assertEqual(['leave'],
+                         ts.get_available_actions({'status': 'assigned'}, perm))
+        self.assertEqual(['leave'],
+                         ts.get_available_actions({'status': 'reopened'}, perm))
+        self.assertEqual(['leave'],
+                         ts.get_available_actions({'status': 'closed'}, perm))
+
+    def test_available_actions_create_only(self):
+        ts = TicketSystem(self.env)
+        perm = Mock(has_permission=lambda x: x == 'TICKET_CREATE')
+        self.assertEqual(['leave'],
+                         ts.get_available_actions({'status': 'new'}, perm))
+        self.assertEqual(['leave'],
+                         ts.get_available_actions({'status': 'assigned'}, perm))
+        self.assertEqual(['leave'],
+                         ts.get_available_actions({'status': 'reopened'}, perm))
+        self.assertEqual(['leave', 'reopen'],
+                         ts.get_available_actions({'status': 'closed'}, perm))
+
+    def test_available_actions_chgprop_only(self):
+        # CHGPROP is not enough for changing a ticket's state (#3289)
+        ts = TicketSystem(self.env)
+        perm = Mock(has_permission=lambda x: x == 'TICKET_CHGPROP')
+        self.assertEqual(['leave'],
+                         ts.get_available_actions({'status': 'new'}, perm))
+        self.assertEqual(['leave'],
+                         ts.get_available_actions({'status': 'assigned'}, perm))
+        self.assertEqual(['leave'],
+                         ts.get_available_actions({'status': 'reopened'}, perm))
+        self.assertEqual(['leave'],
+                         ts.get_available_actions({'status': 'closed'}, perm))
+
+
+def suite():
+    return unittest.makeSuite(TicketSystemTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/tests/conversion.py
@@ -0,0 +1,82 @@
+from trac.test import EnvironmentStub, Mock
+from trac.util import sorted
+from trac.ticket.model import Ticket
+from trac.ticket.web_ui import TicketModule
+from trac.mimeview.api import Mimeview
+from trac.web.clearsilver import HDFWrapper
+from trac.web.href import Href
+
+import unittest
+
+
+class TicketConversionTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.ticket_module = TicketModule(self.env)
+        self.mimeview = Mimeview(self.env)
+        self.req = Mock(hdf=HDFWrapper(['./templates']),
+                        base_path='/trac.cgi', path_info='',
+                        href=Href('/trac.cgi'))
+
+    def _create_a_ticket(self):
+        # 1. Creating ticket
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'santa'
+        ticket['summary'] = 'Foo'
+        ticket['description'] = 'Bar'
+        ticket['foo'] = 'This is a custom field'
+        return ticket
+
+    def test_conversions(self):
+        conversions = self.mimeview.get_supported_conversions(
+            'trac.ticket.Ticket')
+        expected = sorted([('csv', 'Comma-delimited Text', 'csv',
+                           'trac.ticket.Ticket', 'text/csv', 8,
+                           self.ticket_module),
+                          ('tab', 'Tab-delimited Text', 'tsv',
+                           'trac.ticket.Ticket', 'text/tab-separated-values', 8,
+                           self.ticket_module),
+                           ('rss', 'RSS Feed', 'xml',
+                            'trac.ticket.Ticket', 'application/rss+xml', 8,
+                            self.ticket_module)],
+                          key=lambda i: i[-1], reverse=True)
+        self.assertEqual(expected, conversions)
+
+    def test_csv_conversion(self):
+        ticket = self._create_a_ticket()
+        csv = self.mimeview.convert_content(self.req, 'trac.ticket.Ticket',
+                                            ticket, 'csv')
+        self.assertEqual((u'id,summary,reporter,owner,description,keywords,cc'
+                          '\r\nNone,Foo,santa,,Bar,,\r\n',
+                          'text/csv;charset=utf-8', 'csv'), csv)
+
+
+    def test_tab_conversion(self):
+        ticket = self._create_a_ticket()
+        csv = self.mimeview.convert_content(self.req, 'trac.ticket.Ticket',
+                                            ticket, 'tab')
+        self.assertEqual((u'id\tsummary\treporter\towner\tdescription\tkeywords'
+                          '\tcc\r\nNone\tFoo\tsanta\t\tBar\t\t\r\n',
+                          'text/tab-separated-values;charset=utf-8', 'tsv'),
+                         csv)
+
+    def test_rss_conversion(self):
+        ticket = self._create_a_ticket()
+        content, mimetype, ext = self.mimeview.convert_content(
+            self.req, 'trac.ticket.Ticket', ticket, 'rss')
+        self.assertEqual(('<?xml version="1.0"?>\n<!-- RSS generated by Trac v '
+                          'on  -->\n<rss version="2.0">\n <channel>\n   '
+                          '<title>Ticket </title>\n  <link></link>\n  '
+                          '<description>&lt;p&gt;\nBar\n&lt;/p&gt;\n'
+                          '</description>\n  <language>en-us</language>\n  '
+                          '<generator>Trac v</generator>\n </channel>\n</rss>\n',
+                          'application/rss+xml', 'xml'),
+                         (content.replace('\r', ''), mimetype, ext))
+
+
+def suite():
+    return unittest.makeSuite(TicketConversionTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/tests/model.py
@@ -0,0 +1,417 @@
+from trac.config import Configuration
+from trac.core import TracError
+from trac.ticket.model import Ticket, Component, Milestone, Priority, Type
+from trac.test import EnvironmentStub
+
+import unittest
+
+
+class TicketTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub(default_data=True)
+        self.env.config.set('ticket-custom', 'foo', 'text')
+        self.env.config.set('ticket-custom', 'cbon', 'checkbox')
+        self.env.config.set('ticket-custom', 'cboff', 'checkbox')
+
+    def _insert_ticket(self, summary, **kw):
+        """Helper for inserting a ticket into the database"""
+        ticket = Ticket(self.env)
+        for k,v in kw.items():
+            ticket[k] = v
+        return ticket.insert()
+
+    def _create_a_ticket(self):
+        # 1. Creating ticket
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'santa'
+        ticket['summary'] = 'Foo'
+        ticket['foo'] = 'This is a custom field'
+        return ticket
+
+    def test_create_ticket_1(self):
+        ticket = self._create_a_ticket()
+        self.assertEqual('santa', ticket['reporter'])
+        self.assertEqual('Foo', ticket['summary'])
+        self.assertEqual('This is a custom field', ticket['foo'])
+        ticket.insert()
+
+    def test_create_ticket_2(self):
+        ticket = self._create_a_ticket()
+        ticket.insert()
+        # Retrieving ticket
+        ticket2 = Ticket(self.env, 1)
+        self.assertEqual(1, ticket2.id)
+        self.assertEqual('santa', ticket2['reporter'])
+        self.assertEqual('Foo', ticket2['summary'])
+        self.assertEqual('This is a custom field', ticket2['foo'])
+
+    def _modify_a_ticket(self):
+        ticket2 = self._create_a_ticket()
+        ticket2.insert()
+        ticket2['summary'] = 'Bar'
+        ticket2['foo'] = 'New value'
+        ticket2.save_changes('santa', 'this is my comment')
+        return ticket2
+
+    def test_create_ticket_3(self):
+        self._modify_a_ticket()
+        # Retrieving ticket
+        ticket3 = Ticket(self.env, 1)
+        self.assertEqual(1, ticket3.id)
+        self.assertEqual(ticket3['reporter'], 'santa')
+        self.assertEqual(ticket3['summary'], 'Bar')
+        self.assertEqual(ticket3['foo'], 'New value')
+
+    def test_create_ticket_4(self):
+        ticket3 = self._modify_a_ticket()
+        # Testing get_changelog()
+        log = ticket3.get_changelog()
+        self.assertEqual(len(log), 3)
+        ok_vals = ['foo', 'summary', 'comment']
+        self.failUnless(log[0][2] in ok_vals)
+        self.failUnless(log[1][2] in ok_vals)
+        self.failUnless(log[2][2] in ok_vals)
+
+    def test_create_ticket_5(self):
+        ticket3 = self._modify_a_ticket()
+        # Testing delete()
+        ticket3.delete()
+        log = ticket3.get_changelog()
+        self.assertEqual(len(log), 0)
+        self.assertRaises(TracError, Ticket, self.env, 1)
+
+    def test_ticket_default_values(self):
+        """
+        Verify that a ticket uses default values specified in the configuration
+        when created.
+        """
+        # Set defaults for some standard fields
+        self.env.config.set('ticket', 'default_type', 'defect')
+        self.env.config.set('ticket', 'default_component', 'component1')
+
+        # Add a custom field of type 'text' with a default value
+        self.env.config.set('ticket-custom', 'foo', 'text')
+        self.env.config.set('ticket-custom', 'foo.value', 'Something')
+
+        # Add a custom field of type 'select' with a default value specified as
+        # the value itself
+        self.env.config.set('ticket-custom', 'bar', 'select')
+        self.env.config.set('ticket-custom', 'bar.options', 'one|two|three')
+        self.env.config.set('ticket-custom', 'bar.value', 'two')
+
+        # Add a custom field of type 'select' with a default value specified as
+        # index into the options list
+        self.env.config.set('ticket-custom', 'baz', 'select')
+        self.env.config.set('ticket-custom', 'baz.options', 'one|two|three')
+        self.env.config.set('ticket-custom', 'baz.value', '2')
+
+        ticket = Ticket(self.env)
+        self.assertEqual('defect', ticket['type'])
+        self.assertEqual('component1', ticket['component'])
+        self.assertEqual('Something', ticket['foo'])
+        self.assertEqual('two', ticket['bar'])
+        self.assertEqual('three', ticket['baz'])
+
+    def test_set_field_stripped(self):
+        """
+        Verify that whitespace around ticket fields is stripped, except for
+        textarea fields.
+        """
+        ticket = Ticket(self.env)
+        ticket['component'] = '  foo  '
+        ticket['description'] = '  bar  '
+        self.assertEqual('foo', ticket['component'])
+        self.assertEqual('  bar  ', ticket['description'])
+
+    def test_owner_from_component(self):
+        """
+        Verify that the owner of a new ticket is set to the owner of the
+        component.
+        """
+        component = Component(self.env)
+        component.name = 'test'
+        component.owner = 'joe'
+        component.insert()
+
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'santa'
+        ticket['summary'] = 'Foo'
+        ticket['component'] = 'test'
+        ticket.insert()
+        self.assertEqual('joe', ticket['owner'])
+
+    def test_owner_from_changed_component(self):
+        """
+        Verify that the owner of a new ticket is updated when the component is
+        changed.
+        """
+        component1 = Component(self.env)
+        component1.name = 'test1'
+        component1.owner = 'joe'
+        component1.insert()
+
+        component2 = Component(self.env)
+        component2.name = 'test2'
+        component2.owner = 'kate'
+        component2.insert()
+
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'santa'
+        ticket['summary'] = 'Foo'
+        ticket['component'] = 'test1'
+        ticket['status'] = 'new'
+        tktid = ticket.insert()
+
+        ticket = Ticket(self.env, tktid)
+        ticket['component'] = 'test2'
+        ticket.save_changes('jane', 'Testing')
+        self.assertEqual('kate', ticket['owner'])
+
+    def test_populate_ticket(self):
+        data = {'summary': 'Hello world', 'reporter': 'john', 'foo': 'bar',
+                'foo': 'bar', 'checkbox_cbon': '', 'cbon': 'on',
+                'checkbox_cboff': ''}
+        ticket = Ticket(self.env)
+        ticket.populate(data)
+
+        # Standard fields
+        self.assertEqual('Hello world', ticket['summary'])
+        self.assertEqual('john', ticket['reporter'])
+
+        # An unknown field
+        self.assertRaises(KeyError, ticket.__getitem__, 'bar')
+
+        # Custom field
+        self.assertEqual('bar', ticket['foo'])
+
+        # Custom field of type 'checkbox'
+        self.assertEqual('on', ticket['cbon'])
+        self.assertEqual('0', ticket['cboff'])
+
+    def test_changelog(self):
+        tkt_id = self._insert_ticket('Test', reporter='joe', component='foo',
+                                     milestone='bar')
+        ticket = Ticket(self.env, tkt_id)
+        ticket['component'] = 'bar'
+        ticket['milestone'] = 'foo'
+        ticket.save_changes('jane', 'Testing', when=42)
+        for t, author, field, old, new, permanent in ticket.get_changelog():
+            self.assertEqual((42, 'jane', True), (t, author, permanent))
+            if field == 'component':
+                self.assertEqual(('foo', 'bar'), (old, new))
+            elif field == 'milestone':
+                self.assertEqual(('bar', 'foo'), (old, new))
+            elif field == 'comment':
+                self.assertEqual(('', 'Testing'), (old, new))
+            else:
+                self.fail('Unexpected change (%s)'
+                          % ((t, author, field, old, new),))
+
+    def test_changelog_with_reverted_change(self):
+        tkt_id = self._insert_ticket('Test', reporter='joe', component='foo')
+        ticket = Ticket(self.env, tkt_id)
+        ticket['component'] = 'bar'
+        ticket['component'] = 'foo'
+        ticket.save_changes('jane', 'Testing', when=42)
+        for t, author, field, old, new, permanent in ticket.get_changelog():
+            self.assertEqual((42, 'jane', True), (t, author, permanent))
+            if field == 'comment':
+                self.assertEqual(('', 'Testing'), (old, new))
+            else:
+                self.fail('Unexpected change (%s)'
+                          % ((t, author, field, old, new),))
+
+
+class EnumTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub(default_data=True)
+
+    def test_priority_fetch(self):
+        prio = Priority(self.env, 'major')
+        self.assertEqual(prio.name, 'major')
+        self.assertEqual(prio.value, '3')
+
+    def test_priority_insert(self):
+        prio = Priority(self.env)
+        prio.name = 'foo'
+        prio.insert()
+        self.assertEqual(True, prio.exists)
+
+    def test_priority_insert_with_value(self):
+        prio = Priority(self.env)
+        prio.name = 'bar'
+        prio.value = 100
+        prio.insert()
+        self.assertEqual(True, prio.exists)
+
+    def test_priority_update(self):
+        prio = Priority(self.env, 'major')
+        prio.name = 'foo'
+        prio.update()
+        Priority(self.env, 'foo')
+        self.assertRaises(TracError, Priority, self.env, 'major')
+
+    def test_priority_delete(self):
+        prio = Priority(self.env, 'major')
+        prio.delete()
+        self.assertEqual(False, prio.exists)
+        self.assertRaises(TracError, Priority, self.env, 'major')
+
+    def test_ticket_type_update(self):
+        tkttype = Type(self.env, 'task')
+        self.assertEqual(tkttype.name, 'task')
+        self.assertEqual(tkttype.value, '3')
+        tkttype.name = 'foo'
+        tkttype.update()
+        Type(self.env, 'foo')
+
+
+class MilestoneTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub(default_data=True)
+        self.db = self.env.get_db_cnx()
+
+    def test_new_milestone(self):
+        milestone = Milestone(self.env)
+        self.assertEqual(False, milestone.exists)
+        self.assertEqual(None, milestone.name)
+        self.assertEqual(0, milestone.due)
+        self.assertEqual(0, milestone.completed)
+        self.assertEqual('', milestone.description)
+
+    def test_new_milestone_empty_name(self):
+        """
+        Verifies that specifying an empty milestone name results in the
+        milestone being correctly detected as non-existent.
+        """
+        milestone = Milestone(self.env, '')
+        self.assertEqual(False, milestone.exists)
+        self.assertEqual(None, milestone.name)
+        self.assertEqual(0, milestone.due)
+        self.assertEqual(0, milestone.completed)
+        self.assertEqual('', milestone.description)
+
+    def test_existing_milestone(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO milestone (name) VALUES ('Test')")
+        cursor.close()
+
+        milestone = Milestone(self.env, 'Test')
+        self.assertEqual(True, milestone.exists)
+        self.assertEqual('Test', milestone.name)
+        self.assertEqual(0, milestone.due)
+        self.assertEqual(0, milestone.completed)
+        self.assertEqual('', milestone.description)
+
+    def test_create_milestone(self):
+        milestone = Milestone(self.env)
+        milestone.name = 'Test'
+        milestone.insert()
+
+        cursor = self.db.cursor()
+        cursor.execute("SELECT name,due,completed,description FROM milestone "
+                       "WHERE name='Test'")
+        self.assertEqual(('Test', 0, 0, ''), cursor.fetchone())
+
+    def test_create_milestone_without_name(self):
+        milestone = Milestone(self.env)
+        self.assertRaises(AssertionError, milestone.insert)
+
+    def test_delete_milestone(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO milestone (name) VALUES ('Test')")
+        cursor.close()
+
+        milestone = Milestone(self.env, 'Test')
+        milestone.delete()
+
+        cursor = self.db.cursor()
+        cursor.execute("SELECT * FROM milestone WHERE name='Test'")
+        self.assertEqual(None, cursor.fetchone())
+
+    def test_delete_milestone_retarget_tickets(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO milestone (name) VALUES ('Test')")
+        cursor.close()
+
+        tkt1 = Ticket(self.env)
+        tkt1.populate({'summary': 'Foo', 'milestone': 'Test'})
+        tkt1.insert()
+        tkt2 = Ticket(self.env)
+        tkt2.populate({'summary': 'Bar', 'milestone': 'Test'})
+        tkt2.insert()
+
+        milestone = Milestone(self.env, 'Test')
+        milestone.delete(retarget_to='Other')
+
+        self.assertEqual('Other', Ticket(self.env, tkt1.id)['milestone'])
+        self.assertEqual('Other', Ticket(self.env, tkt2.id)['milestone'])
+
+    def test_update_milestone(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO milestone (name) VALUES ('Test')")
+        cursor.close()
+
+        milestone = Milestone(self.env, 'Test')
+        milestone.due = 42
+        milestone.completed = 43
+        milestone.description = 'Foo bar'
+        milestone.update()
+
+        cursor = self.db.cursor()
+        cursor.execute("SELECT * FROM milestone WHERE name='Test'")
+        self.assertEqual(('Test', 42, 43, 'Foo bar'), cursor.fetchone())
+
+    def test_update_milestone_without_name(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO milestone (name) VALUES ('Test')")
+        cursor.close()
+
+        milestone = Milestone(self.env, 'Test')
+        milestone.name = None
+        self.assertRaises(AssertionError, milestone.update)
+
+    def test_update_milestone_update_tickets(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO milestone (name) VALUES ('Test')")
+        cursor.close()
+
+        tkt1 = Ticket(self.env)
+        tkt1.populate({'summary': 'Foo', 'milestone': 'Test'})
+        tkt1.insert()
+        tkt2 = Ticket(self.env)
+        tkt2.populate({'summary': 'Bar', 'milestone': 'Test'})
+        tkt2.insert()
+
+        milestone = Milestone(self.env, 'Test')
+        milestone.name = 'Testing'
+        milestone.update()
+
+        self.assertEqual('Testing', Ticket(self.env, tkt1.id)['milestone'])
+        self.assertEqual('Testing', Ticket(self.env, tkt2.id)['milestone'])
+
+    def test_select_milestones(self):
+        cursor = self.db.cursor()
+        cursor.executemany("INSERT INTO milestone (name) VALUES (%s)",
+                           [('1.0',), ('2.0',)])
+        cursor.close()
+
+        milestones = list(Milestone.select(self.env))
+        self.assertEqual('1.0', milestones[0].name)
+        assert milestones[0].exists
+        self.assertEqual('2.0', milestones[1].name)
+        assert milestones[1].exists
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(TicketTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(EnumTestCase, 'test'))
+    suite.addTest(unittest.makeSuite(MilestoneTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/tests/notification.py
@@ -0,0 +1,511 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2006 Edgewall Software
+# Copyright (C) 2005-2006 Emmanuel Blot <emmanuel.blot@free.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Include a basic SMTP server, based on L. Smithson 
+# (lsmithson@open-networks.co.uk) extensible Python SMTP Server
+#
+
+from trac.config import Configuration
+from trac.core import TracError
+from trac.ticket.model import Ticket
+from trac.ticket.notification import TicketNotifyEmail
+from trac.test import EnvironmentStub, Mock
+from trac.tests.notification import SMTPThreadedServer, parse_smtp_message, \
+                                    smtp_address
+
+import unittest
+import re
+import base64
+import quopri
+import time
+
+
+SMTP_TEST_PORT = 8225
+MAXBODYWIDTH = 76
+notifysuite = None
+
+
+class NotificationTestCase(unittest.TestCase):
+    """Notification test cases that send email over SMTP"""
+    
+    def setUp(self):
+        self.env = EnvironmentStub(default_data=True)
+        self.env.config.set('project',      'name', 'TracTest')
+        self.env.config.set('notification', 'smtp_enabled', 'true')
+        self.env.config.set('notification', 'always_notify_owner', 'true')
+        self.env.config.set('notification', 'always_notify_reporter', 'true')
+        self.env.config.set('notification', 'smtp_always_cc', 
+                            'joe.user@example.net, joe.bar@example.net')
+        self.env.config.set('notification', 'use_public_cc', 'true')
+        self.env.config.set('notification', 'smtp_port', "%d" % SMTP_TEST_PORT)
+        self.env.config.set('notification', 'smtp_server','localhost')
+        self.req = Mock(href=self.env.href, abs_href=self.env.abs_href)
+
+    def tearDown(self):
+        """Signal the notification test suite that a test is over"""
+        notifysuite.tear_down()
+
+    def test_recipients(self):
+        """Validate To/Cc recipients"""
+        ticket = Ticket(self.env)
+        ticket['reporter'] = '"Joe User" <joe.user@example.org>'
+        ticket['owner']    = 'joe.user@example.net'
+        ticket['cc']       = 'joe.user@example.com, joe.bar@example.org, ' \
+                             'joe.bar@example.net'
+        ticket['summary'] = 'Foo'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, self.req, newticket=True)
+        recipients = notifysuite.smtpd.get_recipients()
+        # checks there is no duplicate in the recipient list
+        rcpts = []
+        for r in recipients:
+            self.failIf(r in rcpts)
+            rcpts.append(r)
+        # checks that all cc recipients have been notified
+        cc_list = self.env.config.get('notification', 'smtp_always_cc')
+        cc_list = "%s, %s" % (cc_list, ticket['cc'])
+        for r in cc_list.replace(',', ' ').split():
+            self.failIf(r not in recipients)
+        # checks that owner has been notified
+        self.failIf(smtp_address(ticket['owner']) not in recipients)
+        # checks that reporter has been notified
+        self.failIf(smtp_address(ticket['reporter']) not in recipients)
+
+    def test_no_recipient(self):
+        """Validate no recipient case"""
+        self.env.config.set('notification', 'smtp_always_cc', '')
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'anonymous'
+        ticket['summary'] = 'Foo'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, self.req, newticket=True)
+        sender = notifysuite.smtpd.get_sender()
+        recipients = notifysuite.smtpd.get_recipients()
+        message = notifysuite.smtpd.get_message()
+        # checks that no message has been sent
+        self.failIf(recipients)
+        self.failIf(sender)
+        self.failIf(message)
+
+    def test_cc_only(self):
+        """Validate notifications w/o explicit recipients but Cc: 
+           are actually sent. Non-regression test for #3101"""
+        ticket = Ticket(self.env)
+        ticket['summary'] = 'Foo'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, self.req, newticket=True)
+        recipients = notifysuite.smtpd.get_recipients()
+        # checks that all cc recipients have been notified
+        cc_list = self.env.config.get('notification', 'smtp_always_cc')
+        for r in cc_list.replace(',', ' ').split():
+            self.failIf(r not in recipients)
+
+    def test_structure(self):
+        """Validate basic SMTP message structure (headers, body)"""
+        ticket = Ticket(self.env)
+        ticket['reporter'] = '"Joe User" <joe.user@example.org>'
+        ticket['owner']    = 'joe.user@example.net'
+        ticket['cc']       = 'joe.user@example.com, joe.bar@example.org, ' \
+                             'joe.bar@example.net'
+        ticket['summary'] = 'This is a summary'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, self.req, newticket=True)
+        message = notifysuite.smtpd.get_message()
+        (headers, body) = parse_smtp_message(message)
+        # checks for header existence
+        self.failIf(not headers)
+        # checks for body existance
+        self.failIf(not body)
+        # checks for expected headers
+        self.failIf('Date' not in headers)
+        self.failIf('Subject' not in headers)
+        self.failIf('Message-ID' not in headers)
+        self.failIf('From' not in headers)
+        self.failIf('Sender' not in headers)
+
+    def test_date(self):
+        """Validate date format 
+           Date format hould be compliant with RFC822,
+           we do not support 'military' format""" 
+        date_str = r"^((?P<day>\w{3}),\s*)*(?P<dm>\d{2})\s+" \
+                   r"(?P<month>\w{3})\s+(?P<year>200\d)\s+" \
+                   r"(?P<hour>\d{2}):(?P<min>[0-5][0-9])" \
+                   r"(:(?P<sec>[0-5][0-9]))*\s" \
+                   r"((?P<tz>\w{2,3})|(?P<offset>[+\-]\d{4}))$"
+        date_re = re.compile(date_str)
+        # python time module does not detect incorrect time values
+        days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']
+        months = ['Jan','Feb','Mar','Apr','May','Jun', \
+                  'Jul','Aug','Sep','Oct','Nov','Dec']
+        tz = ['UT','GMT','EST','EDT','CST','CDT','MST','MDT''PST','PDT']
+        ticket = Ticket(self.env)
+        ticket['reporter'] = '"Joe User" <joe.user@example.org>'
+        ticket['summary'] = 'This is a summary'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, self.req, newticket=True)
+        message = notifysuite.smtpd.get_message()
+        (headers, body) = parse_smtp_message(message)
+        self.failIf('Date' not in headers)
+        mo = date_re.match(headers['Date'])
+        self.failIf(not mo)
+        if mo.group('day'):
+            self.failIf(mo.group('day') not in days)
+        self.failIf(int(mo.group('dm')) not in range(1,32))
+        self.failIf(mo.group('month') not in months)
+        self.failIf(int(mo.group('hour')) not in range(0,24))
+        if mo.group('tz'):
+            self.failIf(mo.group('tz') not in tz)
+
+    def test_bcc_privacy(self):
+        """Validate visibility of recipients"""
+        def run_bcc_feature(public):
+            # CC list should be private
+            self.env.config.set('notification', 'use_public_cc',
+                                public and 'true' or 'false')
+            self.env.config.set('notification', 'smtp_always_bcc', 
+                                'joe.foobar@example.net')
+            ticket = Ticket(self.env)
+            ticket['reporter'] = '"Joe User" <joe.user@example.org>'
+            ticket['summary'] = 'This is a summary'
+            ticket.insert()
+            tn = TicketNotifyEmail(self.env)
+            tn.notify(ticket, self.req, newticket=True)
+            message = notifysuite.smtpd.get_message()
+            (headers, body) = parse_smtp_message(message)
+            if public:
+                # Msg should have a To list
+                self.failIf('To' not in headers)
+                # Extract the list of 'To' recipients from the message
+                to = [rcpt.strip() for rcpt in headers['To'].split(',')]
+            else:
+                # Msg should not have a To list
+                self.failIf('To' in headers)
+                # Extract the list of 'To' recipients from the message
+                to = []            
+            # Extract the list of 'Cc' recipients from the message
+            cc = [rcpt.strip() for rcpt in headers['Cc'].split(',')]
+            # Extract the list of the actual SMTP recipients
+            rcptlist = notifysuite.smtpd.get_recipients()
+            # Build the list of the expected 'Cc' recipients 
+            ccrcpt = self.env.config.get('notification', 'smtp_always_cc')
+            cclist = [ccr.strip() for ccr in ccrcpt.split(',')]
+            for rcpt in cclist:
+                # Each recipient of the 'Cc' list should appear in the 'Cc' header
+                self.failIf(rcpt not in cc)
+                # Check the message has actually been sent to the recipients
+                self.failIf(rcpt not in rcptlist)
+            # Build the list of the expected 'Bcc' recipients 
+            bccrcpt = self.env.config.get('notification', 'smtp_always_bcc')
+            bcclist = [bccr.strip() for bccr in bccrcpt.split(',')]
+            for rcpt in bcclist:
+                # Check none of the 'Bcc' recipients appears in the 'To' header
+                self.failIf(rcpt in to)
+                # Check the message has actually been sent to the recipients
+                self.failIf(rcpt not in rcptlist)
+        run_bcc_feature(True)
+        run_bcc_feature(False)
+
+    def test_short_login(self):
+        """Validate email addresses without a FQDN"""
+        def _test_short_login(enabled):
+            ticket = Ticket(self.env)
+            ticket['reporter'] = 'joeuser'
+            ticket['summary'] = 'This is a summary'
+            ticket.insert()
+            # Be sure that at least one email address is valid, so that we 
+            # send a notification even if other addresses are not valid
+            self.env.config.set('notification', 'smtp_always_cc', \
+                                'joe.bar@example.net')
+            if enabled:
+                self.env.config.set('notification', 'use_short_addr', 'true')
+            tn = TicketNotifyEmail(self.env)
+            tn.notify(ticket, self.req, newticket=True)
+            message = notifysuite.smtpd.get_message()
+            (headers, body) = parse_smtp_message(message)
+            # Msg should not have a 'To' header
+            if not enabled:
+                self.failIf('To' in headers)
+            else:
+                tolist = [addr.strip() for addr in headers['To'].split(',')]
+            # Msg should have a 'Cc' field
+            self.failIf('Cc' not in headers)
+            cclist = [addr.strip() for addr in headers['Cc'].split(',')]
+            if enabled:
+                # Msg should be delivered to the reporter
+                self.failIf(ticket['reporter'] not in tolist)
+            else:
+                # Msg should not be delivered to joeuser
+                self.failIf(ticket['reporter'] in cclist)
+            # Msg should still be delivered to the always_cc list
+            self.failIf(self.env.config.get('notification', 'smtp_always_cc') \
+                        not in cclist)
+        # Validate with and without the short addr option enabled
+        for enable in [False, True]:
+            _test_short_login(enable)
+
+    def test_default_domain(self):
+        """Validate support for default domain name"""
+        def _test_default_domain(enabled):
+            self.env.config.set('notification', 'always_notify_owner', 'false')
+            self.env.config.set('notification', 'always_notify_reporter', 'false')
+            self.env.config.set('notification', 'smtp_always_cc', '')
+            ticket = Ticket(self.env)
+            ticket['cc'] = 'joenodom, joewithdom@example.com'
+            ticket['summary'] = 'This is a summary'
+            ticket.insert()
+            # Be sure that at least one email address is valid, so that we 
+            # send a notification even if other addresses are not valid
+            self.env.config.set('notification', 'smtp_always_cc', \
+                                'joe.bar@example.net')
+            if enabled:
+                self.env.config.set('notification', 'smtp_default_domain', 'example.org')
+            tn = TicketNotifyEmail(self.env)
+            tn.notify(ticket, self.req, newticket=True)
+            message = notifysuite.smtpd.get_message()
+            (headers, body) = parse_smtp_message(message)
+            # Msg should always have a 'Cc' field
+            self.failIf('Cc' not in headers)
+            cclist = [addr.strip() for addr in headers['Cc'].split(',')]
+            self.failIf('joewithdom@example.com' not in cclist)
+            self.failIf('joe.bar@example.net' not in cclist)
+            if not enabled:
+                self.failIf(len(cclist) != 2)
+                self.failIf('joenodom' in cclist)
+            else:
+                self.failIf(len(cclist) != 3)
+                self.failIf('joenodom@example.org' not in cclist)
+
+        # Validate with and without a default domain
+        for enable in [False, True]:
+            _test_default_domain(enable)
+
+    def test_email_map(self):
+        """Validate login-to-email map"""
+        self.env.config.set('notification', 'always_notify_owner', 'false')
+        self.env.config.set('notification', 'always_notify_reporter', 'true')
+        self.env.config.set('notification', 'smtp_always_cc', 'joe@example.com')
+        self.env.known_users = [('joeuser', 'Joe User', 'user-joe@example.com')]
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joeuser'
+        ticket['summary'] = 'This is a summary'
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, self.req, newticket=True)
+        message = notifysuite.smtpd.get_message()
+        (headers, body) = parse_smtp_message(message)
+        # Msg should always have a 'To' field
+        self.failIf('To' not in headers)
+        tolist = [addr.strip() for addr in headers['To'].split(',')]
+        # 'To' list should have been resolved to the real email address
+        self.failIf('user-joe@example.com' not in tolist)
+        self.failIf('joeuser' in tolist)
+
+    def test_multiline_header(self):
+        """Validate encoded headers split into multiple lines"""
+        self.env.config.set('notification','mime_encoding', 'qp')
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joe.user@example.org'
+        # Forces non-ascii characters
+        ticket['summary'] = u'A_very %s súmmäry' % u' '.join(['long'] * 20)
+        ticket.insert()
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, self.req, newticket=True)
+        message = notifysuite.smtpd.get_message()
+        (headers, body) = parse_smtp_message(message)
+        # Discards the project name & ticket number
+        subject = headers['Subject']
+        summary = subject[subject.find(':')+2:]
+        self.failIf(ticket['summary'] != summary)
+
+    def test_mimebody_b64(self):
+        """Validate MIME Base64/utf-8 encoding"""
+        self.env.config.set('notification','mime_encoding', 'base64')
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joe.user@example.org'
+        ticket['summary'] = u'This is a long enough summary to cause Trac to' \
+                            u'generate a multi-line súmmäry'
+        ticket.insert()
+        self._validate_mimebody((base64, 'base64', 'utf-8'), \
+                                ticket, True)
+
+    def test_mimebody_qp(self):
+        """Validate MIME QP/utf-8 encoding"""
+        self.env.config.set('notification','mime_encoding', 'qp')
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joe.user@example.org'
+        ticket['summary'] = u'This is a long enough summary to cause Trac to' \
+                            u'generate a multi-line súmmäry'
+        ticket.insert()
+        self._validate_mimebody((quopri, 'quoted-printable', 'utf-8'), \
+                                ticket, True)
+
+    def test_mimebody_none(self):
+        """Validate MIME None/ascii encoding"""
+        self.env.config.set('notification','mime_encoding', 'none')
+        ticket = Ticket(self.env)
+        ticket['reporter'] = 'joe.user@example.org'
+        ticket['summary'] = u'This is a summary'
+        ticket.insert()
+        self._validate_mimebody((None, '7bit', 'ascii'), \
+                                ticket, True)
+
+    def test_updater(self):
+        """Validate no-self-notification option"""
+        def _test_updater(disable):
+            if disable:
+                self.env.config.set('notification','always_notify_updater', 'false')
+            ticket = Ticket(self.env)
+            ticket['reporter'] = 'joe.user@example.org'
+            ticket['summary'] = u'This is a súmmäry'
+            ticket['cc'] = 'joe.bar@example.com'
+            ticket.insert()
+            ticket['component'] = 'dummy'
+            now = time.time()
+            ticket.save_changes('joe.bar2@example.com', 'This is a change', when=now)
+            tn = TicketNotifyEmail(self.env)
+            tn.notify(ticket, self.req, newticket=False, modtime=now)
+            message = notifysuite.smtpd.get_message()
+            (headers, body) = parse_smtp_message(message)
+            # checks for header existence
+            self.failIf(not headers)
+            # checks for updater in the 'To' recipient list
+            self.failIf('To' not in headers)
+            tolist = [addr.strip() for addr in headers['To'].split(',')]
+            if disable:
+                self.failIf('joe.bar2@example.com' in tolist)
+            else:
+                self.failIf('joe.bar2@example.com' not in tolist)
+
+        # Validate with and without a default domain
+        for disable in [False, True]:
+            _test_updater(disable)
+
+    def _validate_mimebody(self, mime, ticket, newtk):
+        """Validate the body of a ticket notification message"""
+        (mime_decoder, mime_name, mime_charset) = mime
+        tn = TicketNotifyEmail(self.env)
+        tn.notify(ticket, self.req, newticket=newtk)
+        message = notifysuite.smtpd.get_message()
+        (headers, body) = parse_smtp_message(message)
+        self.failIf('MIME-Version' not in headers)
+        self.failIf('Content-Type' not in headers)
+        self.failIf('Content-Transfer-Encoding' not in headers)
+        self.failIf(not re.compile(r"1.\d").match(headers['MIME-Version']))
+        type_re = re.compile(r'^text/plain;\scharset="([\w\-\d]+)"$')
+        charset = type_re.match(headers['Content-Type'])
+        self.failIf(not charset)
+        charset = charset.group(1)
+        self.assertEqual(charset, mime_charset)
+        self.assertEqual(headers['Content-Transfer-Encoding'], mime_name)
+        # checks the width of each body line
+        for line in body.splitlines():
+            self.failIf(len(line) > MAXBODYWIDTH)
+        # attempts to decode the body, following the specified MIME endoding 
+        # and charset
+        try:
+            if mime_decoder:
+                body = mime_decoder.decodestring(body)
+            body = unicode(body, charset)
+        except Exception, e:
+            raise AssertionError, e
+        # now processes each line of the body
+        bodylines = body.splitlines()
+        # body starts with a summary line, prefixed with the ticket number
+        # #<n>: summary
+        (tknum, summary) = bodylines[0].split(' ', 1)
+        self.assertEqual(tknum[0], '#')
+        try:
+            tkid = int(tknum[1:-1])
+            self.assertEqual(tkid, 1)
+        except ValueError:
+            raise AssertionError, "invalid ticket number"
+        self.assertEqual(tknum[-1], ':')
+        self.assertEqual(summary, ticket['summary'])
+        # next step: checks the banner appears right after the summary
+        banner_delim_re = re.compile(r'^\-+\+\-+$')
+        self.failIf(not banner_delim_re.match(bodylines[1]))
+        banner = True
+        footer = None
+        props = {}
+        for line in bodylines[2:]:
+            # detect end of banner
+            if banner_delim_re.match(line):
+                banner = False
+                continue
+            if banner:
+                # parse banner and fill in a property dict
+                properties = line.split('|')
+                self.assertEqual(len(properties), 2)
+                for prop in properties:
+                    if prop.strip() == '':
+                        continue
+                    (k, v) = prop.split(':')
+                    props[k.strip().lower()] = v.strip()
+            # detect footer marker (weak detection)
+            if not footer:
+                if line.strip() == '--':
+                    footer = 0
+                    continue
+            # check footer
+            if footer != None:
+                footer += 1
+                # invalid footer detection
+                self.failIf(footer > 3)
+                # check ticket link
+                if line[:11] == 'Ticket URL:':
+                    self.assertEqual(line[12:].strip(), \
+                                     "<%s>" % ticket['link'].strip())
+                # note project title / URL are not validated yet
+
+        # ticket properties which are not expected in the banner
+        xlist = ['summary', 'description', 'link', 'comment']
+        # check banner content (field exists, msg value matches ticket value)
+        for p in [prop for prop in ticket.values.keys() if prop not in xlist]:
+            self.failIf(not props.has_key(p))
+            self.failIf(props[p] != ticket[p])
+
+
+class NotificationTestSuite(unittest.TestSuite):
+    """Thin test suite wrapper to start and stop the SMTP test server"""
+
+    def __init__(self):
+        """Start the local SMTP test server"""
+        unittest.TestSuite.__init__(self)
+        self.smtpd = SMTPThreadedServer(SMTP_TEST_PORT)
+        self.smtpd.start()
+        self.addTest(unittest.makeSuite(NotificationTestCase, 'test'))
+        self.remaining = self.countTestCases()
+
+    def tear_down(self):
+        """Reset the local SMTP test server"""
+        self.smtpd.cleanup()
+        self.remaining = self.remaining-1
+        if self.remaining > 0:
+            return
+        # stop the SMTP test server when all tests have been completed
+        self.smtpd.stop()
+
+def suite():
+    global notifysuite
+    if not notifysuite:
+        notifysuite = NotificationTestSuite()
+    return notifysuite
+
+if __name__ == '__main__':
+    unittest.TextTestRunner(verbosity=2).run(suite())
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/tests/query.py
@@ -0,0 +1,318 @@
+from trac.config import Configuration
+from trac.log import logger_factory
+from trac.test import Mock, EnvironmentStub
+from trac.ticket.query import Query
+
+import unittest
+
+
+class QueryTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub(default_data=True)
+        
+
+    def test_all_ordered_by_id(self):
+        query = Query(self.env, order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_all_ordered_by_id_desc(self):
+        query = Query(self.env, order='id', desc=1)
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+ORDER BY COALESCE(t.id,0)=0 DESC,t.id DESC""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_all_ordered_by_id_verbose(self):
+        query = Query(self.env, order='id', verbose=1)
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.reporter AS reporter,t.description AS description,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_all_ordered_by_priority(self):
+        query = Query(self.env) # priority is default order
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+ORDER BY COALESCE(t.priority,'')='',priority.value,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_all_ordered_by_priority_desc(self):
+        query = Query(self.env, desc=1) # priority is default order
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+ORDER BY COALESCE(t.priority,'')='' DESC,priority.value DESC,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_all_ordered_by_version(self):
+        query = Query(self.env, order='version')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.version AS version,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+  LEFT OUTER JOIN version ON (version.name=version)
+ORDER BY COALESCE(t.version,'')='',COALESCE(version.time,0)=0,version.time,t.version,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_all_ordered_by_version_desc(self):
+        query = Query(self.env, order='version', desc=1)
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.version AS version,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+  LEFT OUTER JOIN version ON (version.name=version)
+ORDER BY COALESCE(t.version,'')='' DESC,COALESCE(version.time,0)=0 DESC,version.time DESC,t.version DESC,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_milestone(self):
+        query = Query.from_string(self.env, 'milestone=milestone1', order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.component AS component,t.time AS time,t.changetime AS changetime,t.milestone AS milestone,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+WHERE COALESCE(t.milestone,'')=%s
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual(['milestone1'], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_all_grouped_by_milestone(self):
+        query = Query(self.env, order='id', group='milestone')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.component AS component,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+  LEFT OUTER JOIN milestone ON (milestone.name=milestone)
+ORDER BY COALESCE(t.milestone,'')='',COALESCE(milestone.due,0)=0,milestone.due,t.milestone,COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_all_grouped_by_milestone_desc(self):
+        query = Query(self.env, order='id', group='milestone', groupdesc=1)
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.component AS component,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+  LEFT OUTER JOIN milestone ON (milestone.name=milestone)
+ORDER BY COALESCE(t.milestone,'')='' DESC,COALESCE(milestone.due,0)=0 DESC,milestone.due DESC,t.milestone DESC,COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_grouped_by_priority(self):
+        query = Query(self.env, group='priority')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.milestone AS milestone,t.component AS component,t.priority AS priority,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+ORDER BY COALESCE(t.priority,'')='',priority.value,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_milestone_not(self):
+        query = Query.from_string(self.env, 'milestone!=milestone1', order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.milestone AS milestone,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+WHERE COALESCE(t.milestone,'')!=%s
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual(['milestone1'], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_status(self):
+        query = Query.from_string(self.env, 'status=new|assigned|reopened',
+                                  order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.status AS status,t.owner AS owner,t.type AS type,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+WHERE COALESCE(t.status,'') IN (%s,%s,%s)
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual(['new', 'assigned', 'reopened'], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_owner_containing(self):
+        query = Query.from_string(self.env, 'owner~=someone', order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+WHERE COALESCE(t.owner,'') LIKE %s
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual(['%someone%'], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_owner_not_containing(self):
+        query = Query.from_string(self.env, 'owner!~=someone', order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+WHERE COALESCE(t.owner,'') NOT LIKE %s
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual(['%someone%'], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_owner_beginswith(self):
+        query = Query.from_string(self.env, 'owner^=someone', order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+WHERE COALESCE(t.owner,'') LIKE %s
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual(['someone%'], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_owner_endswith(self):
+        query = Query.from_string(self.env, 'owner$=someone', order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+WHERE COALESCE(t.owner,'') LIKE %s
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual(['%someone'], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_custom_field(self):
+        self.env.config.set('ticket-custom', 'foo', 'text')
+        query = Query.from_string(self.env, 'foo=something', order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value,foo.value AS foo
+FROM ticket AS t
+  LEFT OUTER JOIN ticket_custom AS foo ON (id=foo.ticket AND foo.name='foo')
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+WHERE COALESCE(foo.value,'')=%s
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual(['something'], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_grouped_by_custom_field(self):
+        self.env.config.set('ticket-custom', 'foo', 'text')
+        query = Query(self.env, group='foo', order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value,foo.value AS foo
+FROM ticket AS t
+  LEFT OUTER JOIN ticket_custom AS foo ON (id=foo.ticket AND foo.name='foo')
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+ORDER BY COALESCE(foo.value,'')='',foo.value,COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_multiple_owners(self):
+        query = Query.from_string(self.env, 'owner=someone|someone_else',
+                                  order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+WHERE COALESCE(t.owner,'') IN (%s,%s)
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual(['someone', 'someone_else'], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_multiple_owners_not(self):
+        query = Query.from_string(self.env, 'owner!=someone|someone_else',
+                                  order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+WHERE COALESCE(t.owner,'') NOT IN (%s,%s)
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual(['someone', 'someone_else'], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_multiple_owners_contain(self):
+        query = Query.from_string(self.env, 'owner~=someone|someone_else',
+                                  order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(['%someone%', '%someone_else%'], args)
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+WHERE (COALESCE(t.owner,'') LIKE %s OR COALESCE(t.owner,'') LIKE %s)
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_empty_value_contains(self):
+        query = Query.from_string(self.env, 'owner~=|', order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_empty_value_startswith(self):
+        query = Query.from_string(self.env, 'owner^=|', order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+    def test_constrained_by_empty_value_endswith(self):
+        query = Query.from_string(self.env, 'owner$=|', order='id')
+        sql, args = query.get_sql()
+        self.assertEqual(sql,
+"""SELECT t.id AS id,t.summary AS summary,t.owner AS owner,t.type AS type,t.status AS status,t.priority AS priority,t.milestone AS milestone,t.time AS time,t.changetime AS changetime,priority.value AS priority_value
+FROM ticket AS t
+  LEFT OUTER JOIN enum AS priority ON (priority.type='priority' AND priority.name=priority)
+ORDER BY COALESCE(t.id,0)=0,t.id""")
+        self.assertEqual([], args)
+        tickets = query.execute(Mock(href=self.env.href))
+
+
+def suite():
+    return unittest.makeSuite(QueryTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/tests/wikisyntax.py
@@ -0,0 +1,184 @@
+import unittest
+
+from trac.ticket.model import Ticket
+from trac.ticket.roadmap import Milestone
+from trac.ticket.query import QueryModule
+from trac.ticket.report import ReportModule
+from trac.wiki.tests import formatter
+
+TICKET_TEST_CASES="""
+============================== ticket: link resolver
+ticket:1
+ticket:12
+ticket:abc
+------------------------------
+<p>
+<a class="new ticket" href="/ticket/1" title="This is the summary (new)">ticket:1</a>
+<a class="missing ticket" href="/ticket/12" rel="nofollow">ticket:12</a>
+<a class="missing ticket" href="/ticket/abc" rel="nofollow">ticket:abc</a>
+</p>
+------------------------------
+============================== ticket link shorthand form
+#1, #2
+#12, #abc
+------------------------------
+<p>
+<a class="new ticket" href="/ticket/1" title="This is the summary (new)">#1</a>, <a class="missing ticket" href="/ticket/2" rel="nofollow">#2</a>
+<a class="missing ticket" href="/ticket/12" rel="nofollow">#12</a>, #abc
+</p>
+------------------------------
+============================== escaping the above
+!#1
+------------------------------
+<p>
+#1
+</p>
+------------------------------
+#1
+============================== InterTrac for tickets
+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">trac:ticket:2041</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/ticket/2041" title="ticket:2041 in Trac's Trac"><span class="icon">Trac #2041</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/ticket/2041" title="ticket:2041 in Trac's Trac"><span class="icon">#T2041</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/ticket/2041" title="ticket:2041 in Trac's Trac"><span class="icon">#trac2041</span></a>
+</p>
+------------------------------
+============================== Ticket InterTrac shorthands
+T:#2041
+trac:#2041
+------------------------------
+<p>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=%232041" title="#2041 in Trac's Trac"><span class="icon">T:#2041</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=%232041" title="#2041 in Trac's Trac"><span class="icon">trac:#2041</span></a>
+</p>
+------------------------------
+"""
+
+def ticket_setup(tc):
+    ticket = Ticket(tc.env)
+    ticket['reporter'] = 'santa'
+    ticket['summary'] = 'This is the summary'
+    ticket.insert()
+    ticket['status'] = 'new'
+    ticket.save_changes('claus', 'set status', 0)
+
+
+
+REPORT_TEST_CASES="""
+============================== report link shorthand form
+{1}, {2}
+{12}, {abc}
+------------------------------
+<p>
+<a class="report" href="/report/1">{1}</a>, <a class="report" href="/report/2">{2}</a>
+<a class="report" href="/report/12">{12}</a>, {abc}
+</p>
+------------------------------
+============================== escaping the above
+!{1}
+------------------------------
+<p>
+{1}
+</p>
+------------------------------
+{1}
+============================== ticket shorthands, not numerical HTML entities
+&#1; &#23;
+------------------------------
+<p>
+&amp;#1; &amp;#23;
+</p>
+------------------------------
+&amp;#1; &amp;#23;
+============================== InterTrac for reports
+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">trac:report:1</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/report/1" title="report:1 in Trac's Trac"><span class="icon">Trac r1</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/report/1" title="report:1 in Trac's Trac"><span class="icon">{T1}</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/report/1" title="report:1 in Trac's Trac"><span class="icon">{trac1}</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/report/1" title="report:1 in Trac's Trac"><span class="icon">{trac 1}</span></a>
+</p>
+------------------------------
+"""
+
+def report_setup(tc):
+    db = tc.env.get_db_cnx()
+    # TBD
+
+
+MILESTONE_TEST_CASES="""
+============================== milestone: link resolver
+milestone:foo
+[milestone:boo Milestone Boo]
+------------------------------
+<p>
+<a class="milestone" href="/milestone/foo">milestone:foo</a>
+<a class="milestone" href="/milestone/boo">Milestone Boo</a>
+</p>
+------------------------------
+"""
+
+
+QUERY_TEST_CASES="""
+============================== query: link resolver
+query:?order=priority
+
+query:?order=priority&owner=me
+
+query:status=new|reopened
+
+query:milestone!=
+
+query:milestone=1.0|2.0&owner=me
+
+query:group=owner
+
+query:verbose=1
+------------------------------
+<p>
+<a class="query" href="/query?order=priority">query:?order=priority</a>
+</p>
+<p>
+<a class="query" href="/query?order=priority&amp;owner=me">query:?order=priority&amp;owner=me</a>
+</p>
+<p>
+<a class="query" href="/query?status=new&amp;status=reopened&amp;order=priority">query:status=new|reopened</a>
+</p>
+<p>
+<a class="query" href="/query?milestone=%21&amp;order=priority">query:milestone!=</a>
+</p>
+<p>
+<a class="query" href="/query?milestone=1.0&amp;milestone=2.0&amp;owner=me&amp;order=priority">query:milestone=1.0|2.0&amp;owner=me</a>
+</p>
+<p>
+<a class="query" href="/query?group=owner&amp;order=priority">query:group=owner</a>
+</p>
+<p>
+<a class="query" href="/query?verbose=1&amp;order=priority">query:verbose=1</a>
+</p>
+------------------------------
+"""
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(formatter.suite(TICKET_TEST_CASES, ticket_setup, __file__))
+    suite.addTest(formatter.suite(REPORT_TEST_CASES, report_setup, __file__))
+    suite.addTest(formatter.suite(MILESTONE_TEST_CASES, file=__file__))
+    suite.addTest(formatter.suite(QUERY_TEST_CASES, file=__file__))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/ticket/web_ui.py
@@ -0,0 +1,667 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+
+import os
+import re
+import time
+from StringIO import StringIO
+
+from trac.attachment import attachments_to_hdf, Attachment, AttachmentModule
+from trac.config import BoolOption, Option
+from trac.core import *
+from trac.env import IEnvironmentSetupParticipant
+from trac.ticket import Milestone, Ticket, TicketSystem, ITicketManipulator
+from trac.ticket.notification import TicketNotifyEmail
+from trac.Timeline import ITimelineEventProvider
+from trac.util import get_reporter_id
+from trac.util.datefmt import format_datetime, pretty_timedelta, http_date
+from trac.util.text import CRLF
+from trac.util.markup import html, Markup
+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
+from trac.mimeview.api import Mimeview, IContentConverter
+
+
+class InvalidTicket(TracError):
+    """Exception raised when a ticket fails validation."""
+
+
+class TicketModuleBase(Component):
+    # FIXME: temporary place-holder for unified ticket validation until
+    #        ticket controller unification is merged
+    abstract = True
+
+    ticket_manipulators = ExtensionPoint(ITicketManipulator)
+
+    def _validate_ticket(self, req, ticket):
+        for manipulator in self.ticket_manipulators:
+            for field, message in manipulator.validate_ticket(req, ticket):
+                if field:
+                    raise InvalidTicket("The ticket %s field is invalid: %s" %
+                                        (field, message))
+                else:
+                    raise InvalidTicket("Invalid ticket: %s" % message)
+
+
+class NewticketModule(TicketModuleBase):
+
+    implements(IEnvironmentSetupParticipant, INavigationContributor,
+               IRequestHandler)
+
+    # IEnvironmentSetupParticipant methods
+
+    def environment_created(self):
+        """Create the `site_newticket.cs` template file in the environment."""
+        if self.env.path:
+            templates_dir = os.path.join(self.env.path, 'templates')
+            if not os.path.exists(templates_dir):
+                os.mkdir(templates_dir)
+            template_name = os.path.join(templates_dir, 'site_newticket.cs')
+            template_file = file(template_name, 'w')
+            template_file.write("""<?cs
+####################################################################
+# New ticket prelude - Included directly above the new ticket form
+?>
+""")
+
+    def environment_needs_upgrade(self, db):
+        return False
+
+    def upgrade_environment(self, db):
+        pass
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'newticket'
+
+    def get_navigation_items(self, req):
+        if not req.perm.has_permission('TICKET_CREATE'):
+            return
+        yield ('mainnav', 'newticket', 
+               html.A('New Ticket', href=req.href.newticket(), accesskey=7))
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        return re.match(r'/newticket/?', req.path_info) is not None
+
+    def process_request(self, req):
+        req.perm.assert_permission('TICKET_CREATE')
+
+        db = self.env.get_db_cnx()
+
+        if req.method == 'POST' and not req.args.has_key('preview'):
+            self._do_create(req, db)
+
+        ticket = Ticket(self.env, db=db)
+        ticket.populate(req.args)
+        ticket.values['reporter'] = get_reporter_id(req, 'reporter')
+
+        if ticket.values.has_key('description'):
+            description = wiki_to_html(ticket['description'], self.env, req, db)
+            req.hdf['newticket.description_preview'] = description
+
+        req.hdf['title'] = 'New Ticket'
+        req.hdf['newticket'] = ticket.values
+
+        field_names = [field['name'] for field in ticket.fields
+                       if not field.get('custom')]
+        if 'owner' in field_names:
+            curr_idx = field_names.index('owner')
+            if 'cc' in field_names:
+                insert_idx = field_names.index('cc')
+            else:
+                insert_idx = len(field_names)
+            if curr_idx < insert_idx:
+                ticket.fields.insert(insert_idx, ticket.fields[curr_idx])
+                del ticket.fields[curr_idx]
+
+        for field in ticket.fields:
+            name = field['name']
+            del field['name']
+            if name in ('summary', 'reporter', 'description', 'type', 'status',
+                        'resolution'):
+                field['skip'] = True
+            elif name == 'owner':
+                field['label'] = 'Assign to'
+            elif name == 'milestone':
+                # Don't make completed milestones available for selection
+                options = field['options'][:]
+                for option in field['options']:
+                    milestone = Milestone(self.env, option, db=db)
+                    if milestone.is_completed:
+                        options.remove(option)
+                field['options'] = options
+            req.hdf['newticket.fields.' + name] = field
+
+        if req.perm.has_permission('TICKET_APPEND'):
+            req.hdf['newticket.can_attach'] = True
+            req.hdf['newticket.attachment'] = req.args.get('attachment')
+
+        add_stylesheet(req, 'common/css/ticket.css')
+        return 'newticket.cs', None
+
+    # Internal methods
+
+    def _do_create(self, req, db):
+        if not req.args.get('summary'):
+            raise TracError('Tickets must contain a summary.')
+
+        ticket = Ticket(self.env, db=db)
+        ticket.populate(req.args)
+        ticket.values['reporter'] = get_reporter_id(req, 'reporter')
+        self._validate_ticket(req, ticket)
+
+        ticket.insert(db=db)
+        db.commit()
+
+        # Notify
+        try:
+            tn = TicketNotifyEmail(self.env)
+            tn.notify(ticket, req, newticket=True)
+        except Exception, e:
+            self.log.exception("Failure sending notification on creation of "
+                               "ticket #%s: %s" % (ticket.id, e))
+
+        # Redirect the user to the newly created ticket
+        if req.args.get('attachment'):
+            req.redirect(req.href.attachment('ticket', ticket.id, action='new'))
+        else:
+            req.redirect(req.href.ticket(ticket.id))
+
+
+class TicketModule(TicketModuleBase):
+
+    implements(INavigationContributor, IRequestHandler, ITimelineEventProvider,
+               IContentConverter)
+
+    default_version = Option('ticket', 'default_version', '',
+        """Default version for newly created tickets.""")
+
+    default_type = Option('ticket', 'default_type', 'defect',
+        """Default type for newly created tickets (''since 0.9'').""")
+
+    default_priority = Option('ticket', 'default_priority', 'major',
+        """Default priority for newly created tickets.""")
+
+    default_milestone = Option('ticket', 'default_milestone', '',
+        """Default milestone for newly created tickets.""")
+
+    default_component = Option('ticket', 'default_component', '',
+        """Default component for newly created tickets""")
+
+    timeline_details = BoolOption('timeline', 'ticket_show_details', 'false',
+        """Enable the display of all ticket changes in the timeline
+        (''since 0.9'').""")
+
+    # IContentConverter methods
+
+    def get_supported_conversions(self):
+        yield ('csv', 'Comma-delimited Text', 'csv',
+               'trac.ticket.Ticket', 'text/csv', 8)
+        yield ('tab', 'Tab-delimited Text', 'tsv',
+               'trac.ticket.Ticket', 'text/tab-separated-values', 8)
+        yield ('rss', 'RSS Feed', 'xml',
+               'trac.ticket.Ticket', 'application/rss+xml', 8)
+
+    def convert_content(self, req, mimetype, ticket, key):
+        if key == 'csv':
+            return self.export_csv(ticket, mimetype='text/csv')
+        elif key == 'tab':
+            return self.export_csv(ticket, sep='\t',
+                                   mimetype='text/tab-separated-values')
+        elif key == 'rss':
+            return self.export_rss(req, ticket)
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'tickets'
+
+    def get_navigation_items(self, req):
+        return []
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        match = re.match(r'/ticket/([0-9]+)', req.path_info)
+        if match:
+            req.args['id'] = match.group(1)
+            return True
+
+    def process_request(self, req):
+        req.perm.assert_permission('TICKET_VIEW')
+
+        action = req.args.get('action', 'view')
+
+        db = self.env.get_db_cnx()
+        id = int(req.args.get('id'))
+
+        ticket = Ticket(self.env, id, db=db)
+
+        if req.method == 'POST':
+            if not req.args.has_key('preview'):
+                self._do_save(req, db, ticket)
+            else:
+                # Use user supplied values
+                ticket.populate(req.args)
+                self._validate_ticket(req, ticket)
+
+                req.hdf['ticket.action'] = action
+                req.hdf['ticket.ts'] = req.args.get('ts')
+                req.hdf['ticket.reassign_owner'] = req.args.get('reassign_owner') \
+                                                   or req.authname
+                req.hdf['ticket.resolve_resolution'] = req.args.get('resolve_resolution')
+                comment = req.args.get('comment')
+                if comment:
+                    req.hdf['ticket.comment'] = comment
+                    # Wiki format a preview of comment
+                    req.hdf['ticket.comment_preview'] = wiki_to_html(
+                        comment, self.env, req, db)
+        else:
+            req.hdf['ticket.reassign_owner'] = req.authname
+            # Store a timestamp in order to detect "mid air collisions"
+            req.hdf['ticket.ts'] = ticket.time_changed
+
+        self._insert_ticket_data(req, db, ticket,
+                                 get_reporter_id(req, 'author'))
+
+        mime = Mimeview(self.env)
+        format = req.args.get('format')
+        if format:
+            mime.send_converted(req, 'trac.ticket.Ticket', ticket, format,
+                                'ticket_%d' % ticket.id)
+
+        # If the ticket is being shown in the context of a query, add
+        # links to help navigate in the query result set
+        if 'query_tickets' in req.session:
+            tickets = req.session['query_tickets'].split()
+            if str(id) in tickets:
+                idx = tickets.index(str(ticket.id))
+                if idx > 0:
+                    add_link(req, 'first', req.href.ticket(tickets[0]),
+                             'Ticket #%s' % tickets[0])
+                    add_link(req, 'prev', req.href.ticket(tickets[idx - 1]),
+                             'Ticket #%s' % tickets[idx - 1])
+                if idx < len(tickets) - 1:
+                    add_link(req, 'next', req.href.ticket(tickets[idx + 1]),
+                             'Ticket #%s' % tickets[idx + 1])
+                    add_link(req, 'last', req.href.ticket(tickets[-1]),
+                             'Ticket #%s' % tickets[-1])
+                add_link(req, 'up', req.session['query_href'])
+
+        add_stylesheet(req, 'common/css/ticket.css')
+
+        # Add registered converters
+        for conversion in mime.get_supported_conversions('trac.ticket.Ticket'):
+            conversion_href = req.href.ticket(ticket.id, format=conversion[0])
+            add_link(req, 'alternate', conversion_href, conversion[1],
+                     conversion[3])
+
+        return 'ticket.cs', None
+
+    # ITimelineEventProvider methods
+
+    def get_timeline_filters(self, req):
+        if req.perm.has_permission('TICKET_VIEW'):
+            yield ('ticket', 'Ticket changes')
+            if self.timeline_details:
+                yield ('ticket_details', 'Ticket details', False)
+
+    def get_timeline_events(self, req, start, stop, filters):
+        format = req.args.get('format')
+
+        status_map = {'new': ('newticket', 'created'),
+                      'reopened': ('newticket', 'reopened'),
+                      'closed': ('closedticket', 'closed'),
+                      'edit': ('editedticket', 'updated')}
+
+        href = format == 'rss' and req.abs_href or req.href
+
+        def produce((id, t, author, type, summary), status, fields,
+                    comment, cid):
+            if status == 'edit':
+                if 'ticket_details' in filters:
+                    info = ''
+                    if len(fields) > 0:
+                        info = ', '.join(['<i>%s</i>' % f for f in \
+                                          fields.keys()]) + ' changed<br />'
+                else:
+                    return None
+            elif 'ticket' in filters:
+                if status == 'closed' and fields.has_key('resolution'):
+                    info = fields['resolution']
+                    if info and comment:
+                        info = '%s: ' % info
+                else:
+                    info = ''
+            else:
+                return None
+            kind, verb = status_map[status]
+            if format == 'rss':
+                title = 'Ticket #%s (%s %s): %s' % \
+                        (id, type.lower(), verb, summary)
+            else:
+                title = Markup('Ticket <em title="%s">#%s</em> (%s) %s by %s',
+                               summary, id, type, verb, author)
+            ticket_href = href.ticket(id)
+            if cid:
+                ticket_href += '#comment:' + cid
+            if status == 'new':
+                message = summary
+            else:
+                message = Markup(info)
+                if comment:
+                    if format == 'rss':
+                        message += wiki_to_html(comment, self.env, req, db,
+                                                absurls=True)
+                    else:
+                        message += wiki_to_oneliner(comment, self.env, db,
+                                                    shorten=True)
+            return kind, ticket_href, title, t, author, message
+
+        # Ticket changes
+        if 'ticket' in filters or 'ticket_details' in filters:
+            db = self.env.get_db_cnx()
+            cursor = db.cursor()
+
+            cursor.execute("SELECT t.id,tc.time,tc.author,t.type,t.summary, "
+                           "       tc.field,tc.oldvalue,tc.newvalue "
+                           "  FROM ticket_change tc "
+                           "    INNER JOIN ticket t ON t.id = tc.ticket "
+                           "      AND tc.time>=%s AND tc.time<=%s "
+                           "ORDER BY tc.time"
+                           % (start, stop))
+            previous_update = None
+            for id,t,author,type,summary,field,oldvalue,newvalue in cursor:
+                if not previous_update or (id,t,author) != previous_update[:3]:
+                    if previous_update:
+                        ev = produce(previous_update, status, fields,
+                                     comment, cid)
+                        if ev:
+                            yield ev
+                    status, fields, comment, cid = 'edit', {}, '', None
+                    previous_update = (id, t, author, type, summary)
+                if field == 'comment':
+                    comment = newvalue
+                    cid = oldvalue and oldvalue.split('.')[-1]
+                elif field == 'status' and newvalue in ('reopened', 'closed'):
+                    status = newvalue
+                else:
+                    fields[field] = newvalue
+            if previous_update:
+                ev = produce(previous_update, status, fields, comment, cid)
+                if ev:
+                    yield ev
+            
+            # New tickets
+            if 'ticket' in filters:
+                cursor.execute("SELECT id,time,reporter,type,summary"
+                               "  FROM ticket WHERE time>=%s AND time<=%s",
+                               (start, stop))
+                for row in cursor:
+                    yield produce(row, 'new', {}, None, None)
+
+            # Attachments
+            if 'ticket_details' in filters:
+                def display(id):
+                    return Markup('ticket %s', html.EM('#', id))
+                att = AttachmentModule(self.env)
+                for event in att.get_timeline_events(req, db, 'ticket',
+                                                     format, start, stop,
+                                                     display):
+                    yield event
+
+    # Internal methods
+
+    def export_csv(self, ticket, sep=',', mimetype='text/plain'):
+        content = StringIO()
+        content.write(sep.join(['id'] + [f['name'] for f in ticket.fields])
+                      + CRLF)
+        content.write(sep.join([unicode(ticket.id)] +
+                                [ticket.values.get(f['name'], '')
+                                 .replace(sep, '_').replace('\\', '\\\\')
+                                 .replace('\n', '\\n').replace('\r', '\\r')
+                                 for f in ticket.fields]) + CRLF)
+        return (content.getvalue(), '%s;charset=utf-8' % mimetype)
+        
+    def export_rss(self, req, ticket):
+        db = self.env.get_db_cnx()
+        changes = []
+        change_summary = {}
+
+        description = wiki_to_html(ticket['description'], self.env, req, db)
+        req.hdf['ticket.description.formatted'] = unicode(description)
+
+        for change in self.grouped_changelog_entries(ticket, db):
+            changes.append(change)
+            # compute a change summary
+            change_summary = {}
+            # wikify comment
+            if 'comment' in change:
+                comment = change['comment']
+                change['comment'] = unicode(wiki_to_html(
+                    comment, self.env, req, db, absurls=True))
+                change_summary['added'] = ['comment']
+            for field, values in change['fields'].iteritems():
+                if field == 'description':
+                    change_summary.setdefault('changed', []).append(field)
+                else:
+                    chg = 'changed'
+                    if not values['old']:
+                        chg = 'set'
+                    elif not values['new']:
+                        chg = 'deleted'
+                    change_summary.setdefault(chg, []).append(field)
+            change['title'] = '; '.join(['%s %s' % (', '.join(v), k) for k, v \
+                                         in change_summary.iteritems()])
+        req.hdf['ticket.changes'] = changes
+        return (req.hdf.render('ticket_rss.cs'), 'application/rss+xml')
+
+
+    def _do_save(self, req, db, ticket):
+        if req.perm.has_permission('TICKET_CHGPROP'):
+            # TICKET_CHGPROP gives permission to edit the ticket
+            if not req.args.get('summary'):
+                raise TracError('Tickets must contain summary.')
+
+            if req.args.has_key('description') or req.args.has_key('reporter'):
+                req.perm.assert_permission('TICKET_ADMIN')
+
+            ticket.populate(req.args)
+        else:
+            req.perm.assert_permission('TICKET_APPEND')
+
+        # Mid air collision?
+        if int(req.args.get('ts')) != ticket.time_changed:
+            raise TracError("Sorry, can not save your changes. "
+                            "This ticket has been modified by someone else "
+                            "since you started", 'Mid Air Collision')
+
+        self._validate_ticket(req, ticket)
+
+        # Do any action on the ticket?
+        action = req.args.get('action')
+        actions = TicketSystem(self.env).get_available_actions(ticket, req.perm)
+        if action not in actions:
+            raise TracError('Invalid action')
+
+        # TODO: this should not be hard-coded like this
+        if action == 'accept':
+            ticket['status'] =  'assigned'
+            ticket['owner'] = req.authname
+        if action == 'resolve':
+            ticket['status'] = 'closed'
+            ticket['resolution'] = req.args.get('resolve_resolution')
+        elif action == 'reassign':
+            ticket['owner'] = req.args.get('reassign_owner')
+            ticket['status'] = 'new'
+        elif action == 'reopen':
+            ticket['status'] = 'reopened'
+            ticket['resolution'] = ''
+
+        now = int(time.time())
+        cnum = req.args.get('cnum')        
+        replyto = req.args.get('replyto')
+        internal_cnum = cnum
+        if cnum and replyto: # record parent.child relationship
+            internal_cnum = '%s.%s' % (replyto, cnum)
+        ticket.save_changes(get_reporter_id(req, 'author'),
+                            req.args.get('comment'), when=now, db=db,
+                            cnum=internal_cnum)
+        db.commit()
+
+        try:
+            tn = TicketNotifyEmail(self.env)
+            tn.notify(ticket, req, newticket=False, modtime=now)
+        except Exception, e:
+            self.log.exception("Failure sending notification on change to "
+                               "ticket #%s: %s" % (ticket.id, e))
+
+        fragment = cnum and '#comment:'+cnum or ''
+        req.redirect(req.href.ticket(ticket.id) + fragment)
+
+    def _insert_ticket_data(self, req, db, ticket, reporter_id):
+        """Insert ticket data into the hdf"""
+        replyto = req.args.get('replyto')
+        req.hdf['title'] = '#%d (%s)' % (ticket.id, ticket['summary'])
+        req.hdf['ticket'] = ticket.values
+        req.hdf['ticket'] = {
+            'id': ticket.id,
+            'href': req.href.ticket(ticket.id),
+            'replyto': replyto
+            }
+
+        # -- Ticket fields
+        
+        for field in TicketSystem(self.env).get_ticket_fields():
+            if field['type'] in ('radio', 'select'):
+                value = ticket.values.get(field['name'])
+                options = field['options']
+                if value and not value in options:
+                    # Current ticket value must be visible even if its not in the
+                    # possible values
+                    options.append(value)
+                field['options'] = options
+            name = field['name']
+            del field['name']
+            if name in ('summary', 'reporter', 'description', 'type', 'status',
+                        'resolution', 'owner'):
+                field['skip'] = True
+            req.hdf['ticket.fields.' + name] = field
+
+        req.hdf['ticket.reporter_id'] = reporter_id
+        req.hdf['ticket.description.formatted'] = wiki_to_html(
+            ticket['description'], self.env, req, db)
+
+        req.hdf['ticket.opened'] = format_datetime(ticket.time_created)
+        req.hdf['ticket.opened_delta'] = pretty_timedelta(ticket.time_created)
+        if ticket.time_changed != ticket.time_created:
+            req.hdf['ticket'] = {
+                'lastmod': format_datetime(ticket.time_changed),
+                'lastmod_delta': pretty_timedelta(ticket.time_changed)
+                }
+
+        # -- Ticket Change History
+
+        def quote_original(author, original, link):
+            if not 'comment' in req.args: # i.e. the comment was not yet edited
+                req.hdf['ticket.comment'] = '\n'.join(
+                    ['Replying to [%s %s]:' % (link, author)] +
+                    ['> %s' % line for line in original.splitlines()] + [''])
+
+        if replyto == 'description':
+            quote_original(ticket['reporter'], ticket['description'],
+                           'ticket:%d' % ticket.id)
+        replies = {}
+        changes = []
+        cnum = 0
+        for change in self.grouped_changelog_entries(ticket, db):
+            changes.append(change)
+            # wikify comment
+            comment = ''
+            if 'comment' in change:
+                comment = change['comment']
+                change['comment'] = wiki_to_html(comment, self.env, req, db)
+            if change['permanent']:
+                cnum = change['cnum']
+                # keep track of replies threading
+                if 'replyto' in change:
+                    replies.setdefault(change['replyto'], []).append(cnum)
+                # eventually cite the replied to comment
+                if replyto == str(cnum):
+                    quote_original(change['author'], comment,
+                                   'comment:%s' % replyto)
+            if 'description' in change['fields']:
+                change['fields']['description'] = ''
+        req.hdf['ticket'] = {
+            'changes': changes,
+            'replies': replies,
+            'cnum': cnum + 1
+           }
+
+        # -- Ticket Attachments
+
+        req.hdf['ticket.attachments'] = attachments_to_hdf(self.env, req, db,
+                                                           'ticket', ticket.id)
+        if req.perm.has_permission('TICKET_APPEND'):
+            req.hdf['ticket.attach_href'] = req.href.attachment('ticket',
+                                                                ticket.id)
+
+        # Add the possible actions to hdf
+        actions = TicketSystem(self.env).get_available_actions(ticket, req.perm)
+        for action in actions:
+            req.hdf['ticket.actions.' + action] = '1'
+
+    def grouped_changelog_entries(self, ticket, db, when=0):
+        """Iterate on changelog entries, consolidating related changes
+        in a `dict` object.
+        """
+        changelog = ticket.get_changelog(when=when, db=db)
+        autonum = 0 # used for "root" numbers
+        last_uid = current = None
+        for date, author, field, old, new, permanent in changelog:
+            uid = date, author, permanent
+            if uid != last_uid:
+                if current:
+                    yield current
+                last_uid = uid
+                current = {
+                    'http_date': http_date(date),
+                    'date': format_datetime(date),
+                    'author': author,
+                    'fields': {},
+                    'permanent': permanent
+                }
+                if permanent and not when:
+                    autonum += 1
+                    current['cnum'] = autonum
+            # some common processing for fields
+            if field == 'comment':
+                current['comment'] = new
+                if old:
+                    if '.' in old: # retrieve parent.child relationship
+                        parent_num, this_num = old.split('.', 1)
+                        current['replyto'] = parent_num
+                    else:
+                        this_num = old
+                    current['cnum'] = int(this_num)
+            else:
+                current['fields'][field] = {'old': old, 'new': new}
+        if current:
+            yield current
new file mode 100644
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db10.py
@@ -0,0 +1,25 @@
+sql = [
+#-- Make the node_change table contain more information, and force a resync
+"""DROP TABLE revision;""",
+"""DROP TABLE node_change;""",
+"""CREATE TABLE revision (
+    rev             text PRIMARY KEY,
+    time            integer,
+    author          text,
+    message         text
+);""",
+"""CREATE TABLE node_change (
+    rev             text,
+    path            text,
+    kind            char(1), -- 'D' for directory, 'F' for file
+    change          char(1),
+    base_path       text,
+    base_rev        text,
+    UNIQUE(rev, path, change)
+);"""
+]
+
+def do_upgrade(env, ver, cursor):
+    for s in sql:
+        cursor.execute(s)
+    print 'Please perform a "resync" after this upgrade.'
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db11.py
@@ -0,0 +1,32 @@
+import os.path
+
+sql = [
+#-- Remove empty values from the milestone list
+"""DELETE FROM milestone WHERE COALESCE(name,'')='';""",
+#-- Add a description column to the version table, and remove unnamed versions
+"""CREATE TEMPORARY TABLE version_old AS SELECT * FROM version;""",
+"""DROP TABLE version;""",
+"""CREATE TABLE version (
+        name            text PRIMARY KEY,
+        time            integer,
+        description     text
+);""",
+"""INSERT INTO version(name,time,description)
+    SELECT name,time,'' FROM version_old WHERE COALESCE(name,'')<>'';""",
+#-- Add a description column to the component table, and remove unnamed components
+"""CREATE TEMPORARY TABLE component_old AS SELECT * FROM component;""",
+"""DROP TABLE component;""",
+"""CREATE TABLE component (
+        name            text PRIMARY KEY,
+        owner           text,
+        description     text
+);""",
+"""INSERT INTO component(name,owner,description)
+    SELECT name,owner,'' FROM component_old WHERE COALESCE(name,'')<>'';""",
+"""DROP TABLE version_old;""",
+"""DROP TABLE component_old;"""
+]
+
+def do_upgrade(env, ver, cursor):
+    for s in sql:
+        cursor.execute(s)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db12.py
@@ -0,0 +1,26 @@
+sql = [
+#-- Some anonymous session might have been left over
+"""DELETE FROM session WHERE username='anonymous';""",
+#-- Schema change: use an authenticated flag instead of separate sid/username
+#-- columns
+"""CREATE TEMPORARY TABLE session_old AS SELECT * FROM session;""",
+"""DROP TABLE session;""",
+"""CREATE TABLE session (
+        sid             text,
+        authenticated   int,
+        var_name        text,
+        var_value       text,
+        UNIQUE(sid, var_name)
+);""",
+"""INSERT INTO session(sid,authenticated,var_name,var_value)
+    SELECT DISTINCT sid,0,var_name,var_value FROM session_old
+    WHERE sid IS NULL;""",
+"""INSERT INTO session(sid,authenticated,var_name,var_value)
+    SELECT DISTINCT username,1,var_name,var_value FROM session_old
+    WHERE sid IS NULL;""",
+"""DROP TABLE session_old;"""
+]
+
+def do_upgrade(env, ver, cursor):
+    for s in sql:
+        cursor.execute(s)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db13.py
@@ -0,0 +1,60 @@
+sql = [
+#-- Add ticket_type to 'ticket', remove the unused 'url' column
+"""CREATE TEMPORARY TABLE ticket_old AS SELECT * FROM ticket;""",
+"""DROP TABLE ticket;""",
+"""CREATE TABLE ticket (
+        id              integer PRIMARY KEY,
+        type            text,           -- the nature of the ticket
+        time            integer,        -- the time it was created
+        changetime      integer,
+        component       text,
+        severity        text,
+        priority        text,
+        owner           text,           -- who is this ticket assigned to
+        reporter        text,
+        cc              text,           -- email addresses to notify
+        version         text,           --
+        milestone       text,           --
+        status          text,
+        resolution      text,
+        summary         text,           -- one-line summary
+        description     text,           -- problem description (long)
+        keywords        text
+);""",
+"""INSERT INTO ticket(id, type, time, changetime, component, severity, priority,
+                   owner, reporter, cc, version, milestone, status, resolution,
+                   summary, description, keywords)
+  SELECT id, 'defect', time, changetime, component, severity, priority, owner,
+         reporter, cc, version, milestone, status, resolution, summary,
+         description, keywords FROM ticket_old
+  WHERE COALESCE(severity,'') <> 'enhancement';""",
+"""INSERT INTO ticket(id, type, time, changetime, component, severity, priority,
+                   owner, reporter, cc, version, milestone, status, resolution,
+                   summary, description, keywords)
+  SELECT id, 'enhancement', time, changetime, component, 'normal', priority,
+         owner, reporter, cc, version, milestone, status, resolution, summary,
+         description, keywords FROM ticket_old
+  WHERE severity = 'enhancement';""",
+"""INSERT INTO enum (type, name, value) VALUES ('ticket_type', 'defect', '1');""",
+"""INSERT INTO enum (type, name, value) VALUES ('ticket_type', 'enhancement', '2');""",
+"""INSERT INTO enum (type, name, value) VALUES ('ticket_type', 'task', '3');""",
+"""DELETE FROM enum WHERE type = 'severity' AND name = 'enhancement';""",
+"""DROP TABLE ticket_old;""",
+]
+
+def do_upgrade(env, ver, cursor):
+    for s in sql:
+        cursor.execute(s)
+
+    # -- upgrade reports (involve a rename)
+    cursor.execute("SELECT id,sql FROM report")
+    reports = {}
+    for id, rsql in cursor:
+        reports[id] = rsql
+    for id, rsql in reports.items():
+        parts = rsql.split('ORDER BY', 1)
+        ending = len(parts)>1 and 'ORDER BY'+parts[1] or ''
+        cursor.execute("UPDATE report SET sql=%s WHERE id=%s",
+                       (parts[0].replace('severity,',
+                                         't.type AS type, severity,') + ending,
+                        id))
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db14.py
@@ -0,0 +1,33 @@
+sql = [
+"""CREATE TEMPORARY TABLE node_change_old AS SELECT * FROM node_change;""",
+"""DROP TABLE node_change;""",
+"""CREATE TABLE node_change (
+    rev             text,
+    path            text,
+    kind            char(1),
+    change          char(1),
+    base_path       text,
+    base_rev        text,
+    UNIQUE(rev, path, change)
+);""",
+"""INSERT INTO node_change (rev,path,kind,change,base_path,base_rev)
+    SELECT rev,path,kind,change,base_path,base_rev FROM node_change_old;""",
+"""DROP TABLE node_change_old;"""
+]
+
+def do_upgrade(env, ver, cursor):
+    # Wiki pages were accidentially created with the version number starting at
+    # 0 instead of 1; This should fix that
+    cursor.execute("SELECT name, version FROM wiki WHERE name IN "
+                   "(SELECT name FROM wiki WHERE version=0) ORDER BY name,"
+                   "version DESC")
+    result = cursor.fetchall()
+    if result:
+        cursor.executemany("UPDATE wiki SET version=version+1 WHERE name=%s " 
+                           "and version=%s",
+                           [tuple(row) for row in result])
+
+    # Correct difference between db_default.py and upgrades/db10.py: The
+    # 'change' was missing from the uniqueness constraint
+    for s in sql:
+        cursor.execute(s)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db15.py
@@ -0,0 +1,20 @@
+from trac.db import Table, Column, Index, DatabaseManager
+
+def do_upgrade(env, ver, cursor):
+    cursor.execute("CREATE TEMPORARY TABLE session_old AS SELECT * FROM session")
+    cursor.execute("DROP TABLE session")
+
+    db = env.get_db_cnx()
+    session_table = Table('session', key=('sid', 'authenticated', 'var_name'))[
+        Column('sid'),
+        Column('authenticated', type='int'),
+        Column('var_name'),
+        Column('var_value')]
+    db_backend, _ = DatabaseManager(env)._get_connector()
+    for stmt in db_backend.to_sql(session_table):
+        cursor.execute(stmt)
+
+    cursor.execute("INSERT INTO session (sid,authenticated,var_name,var_value) "
+                   "SELECT sid,authenticated,var_name,var_value "
+                   "FROM session_old")
+    cursor.execute("DROP TABLE session_old")
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db16.py
@@ -0,0 +1,18 @@
+from trac.db import Table, Column, Index
+
+def do_upgrade(env, ver, cursor):
+    # Add a few new indices to speed things up
+    cursor.execute("CREATE INDEX wiki_time_idx ON wiki (time)")
+    cursor.execute("CREATE INDEX revision_time_idx ON revision (time)")
+    cursor.execute("CREATE INDEX ticket_status_idx ON ticket (status)")
+    cursor.execute("CREATE INDEX ticket_time_idx ON ticket (time)")
+
+    # Fix missing single column primary key constraints
+    if env.config.get('trac', 'database').startswith('postgres'):
+        cursor.execute("ALTER TABLE system ADD CONSTRAINT system_pkey PRIMARY KEY (name)")
+        cursor.execute("ALTER TABLE revision ADD CONSTRAINT revision_pkey PRIMARY KEY (rev)")
+        cursor.execute("ALTER TABLE ticket ADD CONSTRAINT ticket_pkey PRIMARY KEY (id)")
+        cursor.execute("ALTER TABLE component ADD CONSTRAINT component_pkey PRIMARY KEY (name)")
+        cursor.execute("ALTER TABLE milestone ADD CONSTRAINT milestone_pkey PRIMARY KEY (name)")
+        cursor.execute("ALTER TABLE version ADD CONSTRAINT version_pkey PRIMARY KEY (name)")
+        cursor.execute("ALTER TABLE report ADD CONSTRAINT report_pkey PRIMARY KEY (id)")
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db17.py
@@ -0,0 +1,26 @@
+from trac.db import Table, Column, Index, DatabaseManager
+
+def do_upgrade(env, ver, cursor):
+    """Rename the columns `kind` and `change` in the `node_change` table for
+    compatibity with MySQL.
+    """
+    cursor.execute("CREATE TEMPORARY TABLE nc_old AS SELECT * FROM node_change")
+    cursor.execute("DROP TABLE node_change")
+
+    table = Table('node_change', key=('rev', 'path', 'change_type'))[
+        Column('rev'),
+        Column('path'),
+        Column('node_type', size=1),
+        Column('change_type', size=1),
+        Column('base_path'),
+        Column('base_rev'),
+        Index(['rev'])
+    ]
+    db_connector, _ = DatabaseManager(env)._get_connector()
+    for stmt in db_connector.to_sql(table):
+        cursor.execute(stmt)
+
+    cursor.execute("INSERT INTO node_change (rev,path,node_type,change_type,"
+                   "base_path,base_rev) SELECT rev,path,kind,change,"
+                   "base_path,base_rev FROM nc_old")
+    cursor.execute("DROP TABLE nc_old")
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db18.py
@@ -0,0 +1,61 @@
+from trac.db import Table, Column, Index, DatabaseManager
+
+def do_upgrade(env, ver, cursor):
+    cursor.execute("CREATE TEMPORARY TABLE session_old AS SELECT * FROM session")
+    cursor.execute("DROP TABLE session")
+    cursor.execute("CREATE TEMPORARY TABLE ticket_change_old AS SELECT * FROM ticket_change")
+    cursor.execute("DROP TABLE ticket_change")
+
+    # A more normalized session schema where the attributes are stored in
+    # a separate table
+    tables = [Table('session', key=('sid', 'authenticated'))[
+                Column('sid'),
+                Column('authenticated', type='int'),
+                Column('last_visit', type='int'),
+                Index(['last_visit']),
+                Index(['authenticated'])],
+              Table('session_attribute', key=('sid', 'authenticated', 'name'))[
+                Column('sid'),
+                Column('authenticated', type='int'),
+                Column('name'),
+                Column('value')],
+              Table('ticket_change', key=('ticket', 'time', 'field'))[
+                Column('ticket', type='int'),
+                Column('time', type='int'),
+                Column('author'),
+                Column('field'),
+                Column('oldvalue'),
+                Column('newvalue'),
+                Index(['ticket']),
+                Index(['time'])]]
+    
+    db_connector, _ = DatabaseManager(env)._get_connector()
+    for table in tables:
+        for stmt in db_connector.to_sql(table):
+            cursor.execute(stmt)
+
+    # Add an index to the temporary table to speed up the conversion
+    cursor.execute("CREATE INDEX session_old_sid_idx ON session_old(sid)")
+    # Insert the sessions into the new table
+    db = env.get_db_cnx()
+    cursor.execute("INSERT INTO session (sid, last_visit, authenticated) "
+                   "SELECT distinct s.sid,COALESCE(%s,0),s.authenticated "
+                   "FROM session_old AS s LEFT JOIN session_old AS s2 "
+                   "ON (s.sid=s2.sid AND s2.var_name='last_visit') "
+                   "WHERE s.sid IS NOT NULL"
+                   % db.cast('s2.var_value', 'int'))
+    cursor.execute("INSERT INTO session_attribute "
+                   "(sid, authenticated, name, value) "
+                   "SELECT s.sid, s.authenticated, s.var_name, s.var_value "
+                   "FROM session_old s "
+                   "WHERE s.var_name <> 'last_visit' AND s.sid IS NOT NULL")
+
+    # Insert ticket change data into the new table
+    cursor.execute("INSERT INTO ticket_change "
+                   "(ticket, time, author, field, oldvalue, newvalue) "
+                   "SELECT ticket, time, author, field, oldvalue, newvalue "
+                   "FROM ticket_change_old")
+
+    cursor.execute("DROP TABLE session_old")
+    cursor.execute("DROP TABLE ticket_change_old")
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db19.py
@@ -0,0 +1,22 @@
+from trac.db import Table, Column, Index, DatabaseManager
+
+def do_upgrade(env, ver, cursor):
+    """Rename the column `sql` in the `report` table for compatibity with MySQL.
+    """
+    cursor.execute("CREATE TEMPORARY TABLE report_old AS SELECT * FROM report")
+    cursor.execute("DROP TABLE report")
+
+    table = Table('report', key='id')[
+        Column('id', auto_increment=True),
+        Column('author'),
+        Column('title'),
+        Column('query'),
+        Column('description')
+    ]
+    db_connector, _ = DatabaseManager(env)._get_connector()
+    for stmt in db_connector.to_sql(table):
+        cursor.execute(stmt)
+
+    cursor.execute("INSERT INTO report (id,author,title,query,description) "
+                   "SELECT id,author,title,sql,description FROM report_old")
+    cursor.execute("DROP TABLE report_old")
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db3.py
@@ -0,0 +1,18 @@
+sql = """
+CREATE TABLE attachment (
+         type            text,
+         id              text,
+         filename        text,
+         size            integer,
+         time            integer,
+         description     text,
+         author          text,
+         ipnr            text,
+         UNIQUE(type,id,filename)
+);
+"""
+
+def do_upgrade(env, ver, cursor):
+    cursor.execute(sql)
+    env.config.set('attachment', 'max_size', '262144')
+    env.config.save()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db4.py
@@ -0,0 +1,14 @@
+sql = [
+"""CREATE TABLE session (
+         sid             text,
+         username        text,
+         var_name        text,
+         var_value       text,
+         UNIQUE(sid,var_name)
+);""",
+"""CREATE INDEX session_idx ON session(sid,var_name);"""
+]
+
+def do_upgrade(env, ver, cursor):
+    for s in sql:
+        cursor.execute(s)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db5.py
@@ -0,0 +1,18 @@
+sql = [
+#-- Add unique id, descr to 'milestone'
+"""CREATE TEMPORARY TABLE milestone_old AS SELECT * FROM milestone;""",
+"""DROP TABLE milestone;""",
+"""CREATE TABLE milestone (
+         id              integer PRIMARY KEY,
+         name            text,
+         time            integer,
+         descr           text,
+         UNIQUE(name)
+);""",
+"""
+INSERT INTO milestone(name,time, descr) SELECT name,time,'' FROM milestone_old;""",
+"""DROP TABLE milestone_old;""",
+]
+def do_upgrade(env, ver, cursor):
+    for s in sql:
+        cursor.execute(s)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db6.py
@@ -0,0 +1,12 @@
+sql = """
+CREATE TABLE ticket_custom (
+       ticket               integer,
+       name             text,
+       value            text,
+       UNIQUE(ticket,name)
+);
+"""
+
+def do_upgrade(env, ver, cursor):
+    cursor.execute(sql)
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db7.py
@@ -0,0 +1,21 @@
+sql = [
+#-- Add readonly flag to 'wiki'
+"""CREATE TEMPORARY TABLE wiki_old AS SELECT * FROM wiki;""",
+"""DROP TABLE wiki;""",
+"""CREATE TABLE wiki (
+         name            text,
+         version         integer,
+         time            integer,
+         author          text,
+         ipnr            text,
+         text            text,
+         comment         text,
+         readonly        integer,
+         UNIQUE(name,version)
+);""",
+"""INSERT INTO wiki(name,version,time,author,ipnr,text,comment,readonly) SELECT name,version,time,author,ipnr,text,comment,0 FROM wiki_old;"""
+]
+
+def do_upgrade(env, ver, cursor):
+    for s in sql:
+        cursor.execute(s)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db8.py
@@ -0,0 +1,22 @@
+import time
+
+d = {'now':time.time()}
+sql = [
+#-- Separate between due and completed time for milestones.
+"""CREATE TEMPORARY TABLE milestone_old AS SELECT * FROM milestone;""",
+"""DROP TABLE milestone;""",
+"""CREATE TABLE milestone (
+         name            text PRIMARY KEY,
+         due             integer, -- Due date/time
+         completed       integer, -- Completed date/time
+         description     text
+);""",
+"""INSERT INTO milestone(name,due,completed,description)
+SELECT name,time,time,descr FROM milestone_old WHERE time <= %(now)s;""" % d,
+"""INSERT INTO milestone(name,due,description)
+SELECT name,time,descr FROM milestone_old WHERE time > %(now)s;""" % d
+]
+
+def do_upgrade(env, ver, cursor):
+    for s in sql:
+        cursor.execute(s)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/upgrades/db9.py
@@ -0,0 +1,22 @@
+import time
+
+sql = [
+#-- Remove the unused lock table
+"""DROP TABLE lock;""",
+#-- Separate anonymous from authenticated sessions.
+"""CREATE TEMPORARY TABLE session_old AS SELECT * FROM session;""",
+"""DELETE FROM session;""",
+"""INSERT INTO session (username,var_name,var_value)
+  SELECT username,var_name,var_value FROM session_old
+  WHERE sid IN (SELECT DISTINCT sid FROM session_old
+    WHERE username!='anonymous' AND var_name='last_visit'
+    GROUP BY username ORDER BY var_value DESC);""",
+"""INSERT INTO session (sid,username,var_name,var_value)
+  SELECT sid,username,var_name,var_value FROM session_old
+  WHERE username='anonymous';""",
+"""DROP TABLE session_old;"""
+]
+
+def do_upgrade(env, ver, cursor):
+    for s in sql:
+        cursor.execute(s)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/util/__init__.py
@@ -0,0 +1,259 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2006 Matthew Good <trac@matt-good.net>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Matthew Good <trac@matt-good.net>
+
+import locale
+import md5
+import os
+import re
+import sys
+import time
+import tempfile
+from urllib import quote, unquote, urlencode
+
+# Imports for backward compatibility
+from trac.core import TracError
+from trac.util.markup import escape, unescape, Markup, Deuglifier
+from trac.util.text import CRLF, to_utf8, to_unicode, shorten_line, \
+                           wrap, pretty_size
+from trac.util.datefmt import pretty_timedelta, format_datetime, \
+                              format_date, format_time, \
+                              get_date_format_hint, \
+                              get_datetime_format_hint, http_date, \
+                              parse_date
+
+# -- req/session utils
+
+def get_reporter_id(req, arg_name=None):
+    if req.authname != 'anonymous':
+        return req.authname
+    if arg_name:
+        r = req.args.get(arg_name)
+        if r:
+            return r
+    name = req.session.get('name', None)
+    email = req.session.get('email', None)
+    if name and email:
+        return '%s <%s>' % (name, email)
+    return name or email or req.authname # == 'anonymous'
+
+
+# -- algorithmic utilities
+
+try:
+    reversed = reversed
+except NameError:
+    def reversed(x):
+        if hasattr(x, 'keys'):
+            raise ValueError('mappings do not support reverse iteration')
+        i = len(x)
+        while i > 0:
+            i -= 1
+            yield x[i]
+
+try:
+    sorted = sorted
+except NameError:
+    def sorted(iterable, cmp=None, key=None, reverse=False):
+        """Partial implementation of the "sorted" function from Python 2.4"""
+        if key is None:
+            lst = list(iterable)
+        else:
+            lst = [(key(val), idx, val) for idx, val in enumerate(iterable)]
+        lst.sort()
+        if key is None:
+            if reverse:
+                return lst[::-1]
+            return lst
+        if reverse:
+            lst = reversed(lst)
+        return [i[-1] for i in lst]
+
+DIGITS = re.compile(r'(\d+)')
+def embedded_numbers(s):
+    """Comparison function for natural order sorting based on
+    http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/214202."""
+    pieces = DIGITS.split(s)
+    pieces[1::2] = map(int, pieces[1::2])
+    return pieces
+
+
+# -- os utilities
+
+def create_unique_file(path):
+    """Create a new file. An index is added if the path exists"""
+    parts = os.path.splitext(path)
+    idx = 1
+    while 1:
+        try:
+            flags = os.O_CREAT + os.O_WRONLY + os.O_EXCL
+            if hasattr(os, 'O_BINARY'):
+                flags += os.O_BINARY
+            return path, os.fdopen(os.open(path, flags), 'w')
+        except OSError:
+            idx += 1
+            # A sanity check
+            if idx > 100:
+                raise Exception('Failed to create unique name: ' + path)
+            path = '%s.%d%s' % (parts[0], idx, parts[1])
+
+
+class NaivePopen:
+    """This is a deadlock-safe version of popen that returns an object with
+    errorlevel, out (a string) and err (a string).
+
+    The optional `input`, which must be a `str` object, is first written
+    to a temporary file from which the process will read.
+    
+    (`capturestderr` may not work under Windows 9x.)
+
+    Example: print Popen3('grep spam','\n\nhere spam\n\n').out
+    """
+    def __init__(self, command, input=None, capturestderr=None):
+        outfile = tempfile.mktemp()
+        command = '( %s ) > %s' % (command, outfile)
+        if input:
+            infile = tempfile.mktemp()
+            tmp = open(infile, 'w')
+            tmp.write(input)
+            tmp.close()
+            command = command + ' <' + infile
+        if capturestderr:
+            errfile = tempfile.mktemp()
+            command = command + ' 2>' + errfile
+        try:
+            self.err = None
+            self.errorlevel = os.system(command) >> 8
+            outfd = file(outfile, 'r')
+            self.out = outfd.read()
+            outfd.close()
+            if capturestderr:
+                errfd = file(errfile,'r')
+                self.err = errfd.read()
+                errfd.close()
+        finally:
+            if os.path.isfile(outfile):
+                os.remove(outfile)
+            if input and os.path.isfile(infile):
+                os.remove(infile)
+            if capturestderr and os.path.isfile(errfile):
+                os.remove(errfile)
+
+# -- sys utils
+
+def get_last_traceback():
+    import traceback
+    from StringIO import StringIO
+    tb = StringIO()
+    traceback.print_exc(file=tb)
+    return tb.getvalue()
+
+def safe__import__(module_name):
+    """
+    Safe imports: rollback after a failed import.
+    
+    Initially inspired from the RollbackImporter in PyUnit,
+    but it's now much simpler and works better for our needs.
+    
+    See http://pyunit.sourceforge.net/notes/reloading.html
+    """
+    already_imported = sys.modules.copy()
+    try:
+        return __import__(module_name, globals(), locals(), [])
+    except Exception, e:
+        for modname in sys.modules.copy():
+            if not already_imported.has_key(modname):
+                del(sys.modules[modname])
+        raise e
+
+
+# -- crypto utils
+
+def hex_entropy(bytes=32):
+    import md5
+    import random
+    return md5.md5(str(random.random())).hexdigest()[:bytes]
+
+
+# Original license for md5crypt:
+# Based on FreeBSD src/lib/libcrypt/crypt.c 1.2
+#
+# "THE BEER-WARE LICENSE" (Revision 42):
+# <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you
+# can do whatever you want with this stuff. If we meet some day, and you think
+# this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp
+def md5crypt(password, salt, magic='$1$'):
+    # /* The password first, since that is what is most unknown */
+    # /* Then our magic string */
+    # /* Then the raw salt */
+    m = md5.new()
+    m.update(password + magic + salt)
+
+    # /* Then just as many characters of the MD5(pw,salt,pw) */
+    mixin = md5.md5(password + salt + password).digest()
+    for i in range(0, len(password)):
+        m.update(mixin[i % 16])
+
+    # /* Then something really weird... */
+    # Also really broken, as far as I can tell.  -m
+    i = len(password)
+    while i:
+        if i & 1:
+            m.update('\x00')
+        else:
+            m.update(password[0])
+        i >>= 1
+
+    final = m.digest()
+
+    # /* and now, just to make sure things don't run too fast */
+    for i in range(1000):
+        m2 = md5.md5()
+        if i & 1:
+            m2.update(password)
+        else:
+            m2.update(final)
+
+        if i % 3:
+            m2.update(salt)
+
+        if i % 7:
+            m2.update(password)
+
+        if i & 1:
+            m2.update(final)
+        else:
+            m2.update(password)
+
+        final = m2.digest()
+
+    # This is the bit that uses to64() in the original code.
+
+    itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+
+    rearranged = ''
+    for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
+        v = ord(final[a]) << 16 | ord(final[b]) << 8 | ord(final[c])
+        for i in range(4):
+            rearranged += itoa64[v & 0x3f]; v >>= 6
+
+    v = ord(final[11])
+    for i in range(2):
+        rearranged += itoa64[v & 0x3f]; v >>= 6
+
+    return magic + salt + '$' + rearranged
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/util/autoreload.py
@@ -0,0 +1,82 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import os
+import sys
+import time
+import thread
+
+_SLEEP_TIME = 1
+
+def _reloader_thread(modification_callback):
+    """When this function is run from the main thread, it will force other
+    threads to exit when any modules currently loaded change.
+    @param modification_callback: Function taking a single argument, the
+    modified file, and is called after a modification is detected."""
+    mtimes = {}
+    while True:
+        for filename in filter(None, [getattr(module, "__file__", None)
+                                      for module in sys.modules.values()]):
+            while not os.path.isfile(filename): # Probably in an egg or zip file
+                filename = os.path.dirname(filename)
+                if not filename:
+                    break
+            if not filename: # Couldn't map to physical file, so just ignore
+                continue
+
+            if filename.endswith(".pyc"):
+                filename = filename[:-1]
+
+            mtime = os.stat(filename).st_mtime
+            if filename not in mtimes:
+                mtimes[filename] = mtime
+                continue
+            if mtime > mtimes[filename]:
+                modification_callback(filename)
+                sys.exit(3)
+        time.sleep(_SLEEP_TIME)
+
+def _restart_with_reloader():
+    while True:
+        args = [sys.executable] + sys.argv
+        if sys.platform == "win32":
+            args = ['"%s"' % arg for arg in args]
+        new_environ = os.environ.copy()
+        new_environ["RUN_MAIN"] = 'true'
+
+        # This call reinvokes ourself and goes into the other branch of main as
+        # a new process.
+        exit_code = os.spawnve(os.P_WAIT, sys.executable,
+                               args, new_environ)
+        if exit_code != 3:
+            return exit_code
+
+def main(main_func, modification_callback):
+    """Run `main_func` and restart any time modules are changed."""
+
+    if os.environ.get("RUN_MAIN"):
+        # Lanch the actual program as a child thread
+        thread.start_new_thread(main_func, ())
+
+        try:
+            # Now wait for a file modification and quit
+            _reloader_thread(modification_callback)
+        except KeyboardInterrupt:
+            pass
+
+    else:
+        # Initial invocation just waits around restarting this executable
+        try:
+            sys.exit(_restart_with_reloader())
+        except KeyboardInterrupt:
+            pass
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/util/daemon.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import os
+import sys
+
+def daemonize(stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
+    """Fork a daemon process (taken from the Python Cookbook)."""
+
+    # Perform first fork
+    pid = os.fork()
+    if pid > 0:
+        sys.exit(0) # exit first parent
+
+    # Decouple from parent environment
+    os.chdir('/')
+    os.umask(0)
+    os.setsid()
+
+    # Perform second fork
+    pid = os.fork()
+    if pid > 0:
+        sys.exit(0) # exit first parent
+
+    # The process is now daemonized, redirect standard file descriptors
+    for fileobj in sys.stdout, sys.stderr:
+        fileobj.flush()
+    stdin = file(stdin, 'r')
+    stdout = file(stdout, 'a+')
+    stderr = file(stderr, 'a+', 0)
+    os.dup2(stdin.fileno(), sys.stdin.fileno())
+    os.dup2(stdout.fileno(), sys.stdout.fileno())
+    os.dup2(stderr.fileno(), sys.stderr.fileno())
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/util/datefmt.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2006 Matthew Good <trac@matt-good.net>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Matthew Good <trac@matt-good.net>
+
+import locale
+import sys
+import time
+
+# Date/time utilities
+
+def pretty_timedelta(time1, time2=None, resolution=None):
+    """Calculate time delta (inaccurately, only for decorative purposes ;-) for
+    prettyprinting. If time1 is None, the current time is used."""
+    if not time1: time1 = time.time()
+    if not time2: time2 = time.time()
+    if time1 > time2:
+        time2, time1 = time1, time2
+    units = ((3600 * 24 * 365, 'year',   'years'),
+             (3600 * 24 * 30,  'month',  'months'),
+             (3600 * 24 * 7,   'week',   'weeks'),
+             (3600 * 24,       'day',    'days'),
+             (3600,            'hour',   'hours'),
+             (60,              'minute', 'minutes'))
+    age_s = int(time2 - time1)
+    if resolution and age_s < resolution:
+        return ''
+    if age_s < 60:
+        return '%i second%s' % (age_s, age_s != 1 and 's' or '')
+    for u, unit, unit_plural in units:
+        r = float(age_s) / float(u)
+        if r >= 0.9:
+            r = int(round(r))
+            return '%d %s' % (r, r == 1 and unit or unit_plural)
+    return ''
+
+def format_datetime(t=None, format='%x %X', gmt=False):
+    if t is None:
+        t = time.time()
+    if not isinstance(t, (list, tuple, time.struct_time)):
+        if gmt:
+            t = time.gmtime(int(t))
+        else:
+            t = time.localtime(int(t))
+
+    text = time.strftime(format, t)
+    encoding = locale.getpreferredencoding()
+    if sys.platform != 'win32':
+        encoding = locale.getlocale(locale.LC_TIME)[1] or encoding
+        # the above is broken on win32, e.g. we'd get '437' instead of 'cp437'
+    return unicode(text, encoding, 'replace')
+
+def format_date(t=None, format='%x', gmt=False):
+    return format_datetime(t, format, gmt)
+
+def format_time(t=None, format='%X', gmt=False):
+    return format_datetime(t, format, gmt)
+
+def get_date_format_hint():
+    t = time.localtime(0)
+    t = (1999, 10, 29, t[3], t[4], t[5], t[6], t[7], t[8])
+    tmpl = format_date(t)
+    return tmpl.replace('1999', 'YYYY', 1).replace('99', 'YY', 1) \
+               .replace('10', 'MM', 1).replace('29', 'DD', 1)
+
+def get_datetime_format_hint():
+    t = time.localtime(0)
+    t = (1999, 10, 29, 23, 59, 58, t[6], t[7], t[8])
+    tmpl = format_datetime(t)
+    return tmpl.replace('1999', 'YYYY', 1).replace('99', 'YY', 1) \
+               .replace('10', 'MM', 1).replace('29', 'DD', 1) \
+               .replace('23', 'hh', 1).replace('59', 'mm', 1) \
+               .replace('58', 'ss', 1)
+
+def http_date(t=None):
+    """Format t as a rfc822 timestamp"""
+    if t is None:
+        t = time.time()
+    if not isinstance(t, (list, tuple, time.struct_time)):
+        t = time.gmtime(int(t))
+    weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+    months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
+              'Oct', 'Nov', 'Dec']
+    return '%s, %02d %s %04d %02d:%02d:%02d GMT' % (
+           weekdays[t.tm_wday], t.tm_mday, months[t.tm_mon - 1], t.tm_year,
+           t.tm_hour, t.tm_min, t.tm_sec)
+
+def parse_date(text):
+    seconds = None
+    text = text.strip()
+    for format in ['%x %X', '%x, %X', '%X %x', '%X, %x', '%x', '%c',
+                   '%b %d, %Y']:
+        try:
+            date = time.strptime(text, format)
+            seconds = time.mktime(date)
+            break
+        except ValueError:
+            continue
+    if seconds == None:
+        raise ValueError, '%s is not a known date format.' % text
+    return seconds
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/util/markup.py
@@ -0,0 +1,467 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import htmlentitydefs
+from HTMLParser import HTMLParser, HTMLParseError
+import re
+try:
+    frozenset
+except NameError:
+    from sets import ImmutableSet as frozenset
+from StringIO import StringIO
+import sys
+
+__all__ = ['escape', 'unescape', 'html']
+
+_EMPTY_TAGS = frozenset(['br', 'hr', 'img', 'input'])
+_BOOLEAN_ATTRS = frozenset(['selected', 'checked', 'compact', 'declare',
+                            'defer', 'disabled', 'ismap', 'multiple', 'nohref',
+                            'noresize', 'noshade', 'nowrap'])
+
+
+class Markup(unicode):
+    """Marks a string as being safe for inclusion in XML output without needing
+    to be escaped.
+    
+    Strings are normally automatically escaped when added to the HDF.
+    `Markup`-strings are however an exception. Use with care.
+    
+    (since Trac 0.9.3)
+    """
+    def __new__(self, text='', *args):
+        if args:
+            text %= tuple([escape(arg) for arg in args])
+        return unicode.__new__(self, text)
+
+    def __add__(self, other):
+        return Markup(unicode(self) + Markup.escape(other))
+
+    def __mod__(self, args):
+        if not isinstance(args, (list, tuple)):
+            args = [args]
+        return Markup(unicode.__mod__(self,
+                                      tuple([escape(arg) for arg in args])))
+
+    def __mul__(self, num):
+        return Markup(unicode(self) * num)
+
+    def join(self, seq):
+        return Markup(unicode(self).join([Markup.escape(item) for item in seq]))
+
+    def stripentities(self, keepxmlentities=False):
+        """Return a copy of the text with any character or numeric entities
+        replaced by the equivalent UTF-8 characters.
+        
+        If the `keepxmlentities` parameter is provided and evaluates to `True`,
+        the core XML entities (&amp;, &apos;, &gt;, &lt; and &quot;).
+        
+        (Since Trac 0.10)
+        """
+        def _replace_entity(match):
+            if match.group(1): # numeric entity
+                ref = match.group(1)
+                if ref.startswith('x'):
+                    ref = int(ref[1:], 16)
+                else:
+                    ref = int(ref, 10)
+                return unichr(ref)
+            else: # character entity
+                ref = match.group(2)
+                if keepxmlentities and ref in ('amp', 'apos', 'gt', 'lt', 'quot'):
+                    return '&%s;' % ref
+                try:
+                    codepoint = htmlentitydefs.name2codepoint[ref]
+                    return unichr(codepoint)
+                except KeyError:
+                    if keepxmlentities:
+                        return '&amp;%s;' % ref
+                    else:
+                        return ref
+        return Markup(re.sub(r'&(?:#((?:\d+)|(?:[xX][0-9a-fA-F]+));?|(\w+);)',
+                             _replace_entity, self))
+
+    def striptags(self):
+        """Return a copy of the text with all XML/HTML tags removed."""
+        return Markup(re.sub(r'<[^>]*?>', '', self))
+
+    def escape(cls, text, quotes=True):
+        """Create a Markup instance from a string and escape special characters
+        it may contain (<, >, & and \").
+        
+        If the `quotes` parameter is set to `False`, the \" character is left
+        as is. Escaping quotes is generally only required for strings that are
+        to be used in attribute values.
+        """
+        if isinstance(text, (cls, Element)):
+            return text
+        text = unicode(text)
+        if not text:
+            return cls()
+        text = text.replace('&', '&amp;') \
+                   .replace('<', '&lt;') \
+                   .replace('>', '&gt;')
+        if quotes:
+            text = text.replace('"', '&#34;')
+        return cls(text)
+    escape = classmethod(escape)
+
+    def unescape(self):
+        """Reverse-escapes &, <, > and \" and returns a `unicode` object."""
+        if not self:
+            return ''
+        return unicode(self).replace('&#34;', '"') \
+                            .replace('&gt;', '>') \
+                            .replace('&lt;', '<') \
+                            .replace('&amp;', '&')
+
+    def plaintext(self, keeplinebreaks=True):
+        """Returns the text as a `unicode`with all entities and tags removed."""
+        text = unicode(self.striptags().stripentities())
+        if not keeplinebreaks:
+            text = text.replace('\n', ' ')
+        return text
+
+    def sanitize(self):
+        """Parse the text as HTML and return a cleaned up XHTML representation.
+        
+        This will remove any javascript code or other potentially dangerous
+        elements.
+        
+        If the HTML cannot be parsed, an `HTMLParseError` will be raised by the
+        underlying `HTMLParser` module, which should be handled by the caller of
+        this function.
+        """
+        buf = StringIO()
+        sanitizer = HTMLSanitizer(buf)
+        sanitizer.feed(self.stripentities(keepxmlentities=True))
+        return Markup(buf.getvalue())
+
+
+escape = Markup.escape
+
+def unescape(text):
+    """Reverse-escapes &, <, > and \" and returns a `unicode` object."""
+    if not isinstance(text, Markup):
+        return text
+    return text.unescape()
+
+
+class Deuglifier(object):
+
+    def __new__(cls):
+        self = object.__new__(cls)
+        if not hasattr(cls, '_compiled_rules'):
+            cls._compiled_rules = re.compile('(?:' + '|'.join(cls.rules()) + ')')
+        self._compiled_rules = cls._compiled_rules
+        return self
+    
+    def format(self, indata):
+        return re.sub(self._compiled_rules, self.replace, indata)
+
+    def replace(self, fullmatch):
+        for mtype, match in fullmatch.groupdict().items():
+            if match:
+                if mtype == 'font':
+                    return '<span>'
+                elif mtype == 'endfont':
+                    return '</span>'
+                return '<span class="code-%s">' % mtype
+
+
+class HTMLSanitizer(HTMLParser):
+
+    safe_tags = frozenset(['a', 'abbr', 'acronym', 'address', 'area',
+        'b', 'big', 'blockquote', 'br', 'button', 'caption', 'center',
+        'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir',
+        'div', 'dl', 'dt', 'em', 'fieldset', 'font', 'form', 'h1', 'h2',
+        'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', 'kbd',
+        'label', 'legend', 'li', 'map', 'menu', 'ol', 'optgroup',
+        'option', 'p', 'pre', 'q', 's', 'samp', 'select', 'small',
+        'span', 'strike', 'strong', 'sub', 'sup', 'table', 'tbody',
+        'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u', 'ul',
+        'var'])
+    safe_attrs = frozenset(['abbr', 'accept', 'accept-charset',
+        'accesskey', 'action', 'align', 'alt', 'axis', 'border', 'bgcolor',
+        'cellpadding', 'cellspacing', 'char', 'charoff', 'charset',
+        'checked', 'cite', 'class', 'clear', 'cols', 'colspan', 'color',
+        'compact', 'coords', 'datetime', 'dir', 'disabled', 'enctype',
+        'for', 'frame', 'headers', 'height', 'href', 'hreflang',
+        'hspace', 'id', 'ismap', 'label', 'lang', 'longdesc',
+        'maxlength', 'media', 'method', 'multiple', 'name', 'nohref',
+        'noshade', 'nowrap', 'prompt', 'readonly', 'rel', 'rev', 'rows',
+        'rowspan', 'rules', 'scope', 'selected', 'shape', 'size',
+        'span', 'src', 'start', 'style', 'summary', 'tabindex',
+        'target', 'title', 'type', 'usemap', 'valign', 'value',
+        'vspace', 'width'])
+    uri_attrs = frozenset(['action', 'background', 'dynsrc', 'href',
+                           'lowsrc', 'src'])
+    safe_schemes = frozenset(['file', 'ftp', 'http', 'https', 'mailto',
+                              None])
+
+    def __init__(self, out):
+        HTMLParser.__init__(self)
+        self.out = out
+        self.waiting_for = None
+
+    def handle_starttag(self, tag, attrs):
+        if self.waiting_for:
+            return
+        if tag not in self.safe_tags:
+            self.waiting_for = tag
+            return
+        self.out.write('<' + tag)
+
+        def _get_scheme(text):
+            if ':' not in text:
+                return None
+            chars = [char for char in text.split(':', 1)[0]
+                     if char.isalnum()]
+            return ''.join(chars).lower()
+
+        for attrname, attrval in attrs:
+            if attrname not in self.safe_attrs:
+                continue
+            elif attrname in self.uri_attrs:
+                # Don't allow URI schemes such as "javascript:"
+                if _get_scheme(attrval) not in self.safe_schemes:
+                    continue
+            elif attrname == 'style':
+                # Remove dangerous CSS declarations from inline styles
+                decls = []
+                for decl in filter(None, attrval.split(';')):
+                    is_evil = False
+                    if 'expression' in decl:
+                        is_evil = True
+                    for m in re.finditer(r'url\s*\(([^)]+)', decl):
+                        if _get_scheme(m.group(1)) not in self.safe_schemes:
+                            is_evil = True
+                            break
+                    if not is_evil:
+                        decls.append(decl.strip())
+                if not decls:
+                    continue
+                attrval = '; '.join(decls)
+            self.out.write(' ' + attrname + '="' + escape(attrval) + '"')
+
+        if tag in _EMPTY_TAGS:
+            self.out.write(' />')
+        else:
+            self.out.write('>')
+
+    def handle_entityref(self, name):
+        if not self.waiting_for:
+            self.out.write('&%s;' % name)
+
+    def handle_data(self, data):
+        if not self.waiting_for:
+            self.out.write(escape(data, quotes=False))
+
+    def handle_endtag(self, tag):
+        if self.waiting_for:
+            if self.waiting_for == tag:
+                self.waiting_for = None
+            return
+        if tag not in _EMPTY_TAGS:
+            self.out.write('</' + tag + '>')
+
+
+class Fragment(object):
+    __slots__ = ['children']
+
+    def __init__(self):
+        self.children = []
+
+    def append(self, node):
+        """Append an element or string as child node."""
+        if isinstance(node, (Element, Markup, basestring, int, float, long)):
+            # For objects of a known/primitive type, we avoid the check for
+            # whether it is iterable for better performance
+            self.children.append(node)
+        elif isinstance(node, Fragment):
+            self.children += node.children
+        elif node is not None:
+            try:
+                for child in node:
+                    self.append(child)
+            except TypeError:
+                self.children.append(node)
+
+    def __call__(self, *args):
+        for arg in args:
+            self.append(arg)
+        return self
+
+    def serialize(self):
+        """Generator that yield tags and text nodes as strings."""
+        for child in self.children:
+            if isinstance(child, Fragment):
+                yield unicode(child)
+            else:
+                yield escape(child, quotes=False)
+
+    def __str__(self):
+        return Markup(''.join(self.serialize()))
+
+    def __add__(self, other):
+        return Fragment()(self, other)
+
+
+class Element(Fragment):
+    """Simple XHTML output generator based on the builder pattern.
+    
+    Construct XHTML elements by passing the tag name to the constructor:
+    
+    >>> print Element('strong')
+    <strong></strong>
+    
+    Attributes can be specified using keyword arguments. The values of the
+    arguments will be converted to strings and any special XML characters
+    escaped:
+    
+    >>> print Element('textarea', rows=10, cols=60)
+    <textarea rows="10" cols="60"></textarea>
+    >>> print Element('span', title='1 < 2')
+    <span title="1 &lt; 2"></span>
+    >>> print Element('span', title='"baz"')
+    <span title="&#34;baz&#34;"></span>
+    
+    The " character is escaped using a numerical entity.
+    The order in which attributes are rendered is undefined.
+    
+    If an attribute value evaluates to `None`, that attribute is not included
+    in the output:
+    
+    >>> print Element('a', name=None)
+    <a></a>
+    
+    Attribute names that conflict with Python keywords can be specified by
+    appending an underscore:
+    
+    >>> print Element('div', class_='warning')
+    <div class="warning"></div>
+    
+    While the tag names and attributes are not restricted to the XHTML language,
+    some HTML characteristics such as boolean (minimized) attributes and empty
+    elements get special treatment.
+    
+    For compatibility with HTML user agents, some XHTML elements need to be
+    closed using a separate closing tag even if they are empty. For this, the
+    close tag is only ommitted for a small set of elements which are known be
+    be safe for use as empty elements:
+    
+    >>> print Element('br')
+    <br />
+    
+    Trying to add nested elements to such an element will cause an
+    `AssertionError`:
+    
+    >>> Element('br')('Oops')
+    Traceback (most recent call last):
+        ...
+    AssertionError: 'br' elements must not have content
+    
+    Furthermore, boolean attributes such as "selected" or "checked" are omitted
+    if the value evaluates to `False`. Otherwise, the name of the attribute is
+    used for the value:
+    
+    >>> print Element('option', value=0, selected=False)
+    <option value="0"></option>
+    >>> print Element('option', selected='yeah')
+    <option selected="selected"></option>
+    
+    
+    Nested elements can be added to an element by calling the instance using
+    positional arguments. The same technique can also be used for adding
+    attributes using keyword arguments, as one would do in the constructor:
+    
+    >>> print Element('ul')(Element('li'), Element('li'))
+    <ul><li></li><li></li></ul>
+    >>> print Element('a')('Label')
+    <a>Label</a>
+    >>> print Element('a')('Label', href="target")
+    <a href="target">Label</a>
+
+    Text nodes can be nested in an element by adding strings instead of
+    elements. Any special characters in the strings are escaped automatically:
+
+    >>> print Element('em')('Hello world')
+    <em>Hello world</em>
+    >>> print Element('em')(42)
+    <em>42</em>
+    >>> print Element('em')('1 < 2')
+    <em>1 &lt; 2</em>
+
+    This technique also allows mixed content:
+
+    >>> print Element('p')('Hello ', Element('b')('world'))
+    <p>Hello <b>world</b></p>
+
+    Elements can also be combined with other elements or strings using the
+    addition operator, which results in a `Fragment` object that contains the
+    operands:
+    
+    >>> print Element('br') + 'some text' + Element('br')
+    <br />some text<br />
+    """
+    __slots__ = ['tagname', 'attr']
+
+    def __init__(self, tagname_=None, **attr):
+        Fragment.__init__(self)
+        if tagname_:
+            self.tagname = tagname_
+        self.attr = {}
+        self(**attr)
+
+    def __call__(self, *args, **attr):
+        self.attr.update(attr)
+        return Fragment.__call__(self, *args)
+
+    def append(self, node):
+        """Append an element or string as child node."""
+        assert self.tagname not in _EMPTY_TAGS, \
+            "'%s' elements must not have content" % self.tagname
+        Fragment.append(self, node)
+
+    def serialize(self):
+        """Generator that yield tags and text nodes as strings."""
+        starttag = ['<', self.tagname]
+        for name, value in self.attr.items():
+            if value is None:
+                continue
+            if name in _BOOLEAN_ATTRS:
+                if not value:
+                    continue
+                value = name
+            else:
+                name = name.rstrip('_').replace('_', '-')
+            starttag.append(' %s="%s"' % (name.lower(), escape(value)))
+
+        if self.children or self.tagname not in _EMPTY_TAGS:
+            starttag.append('>')
+            yield Markup(''.join(starttag))
+            for part in Fragment.serialize(self):
+                yield part
+            yield Markup('</%s>', self.tagname)
+
+        else:
+            starttag.append(' />')
+            yield Markup(''.join(starttag))
+
+
+class Tags(object):
+
+    def __getattribute__(self, name):
+        return Element(name.lower())
+
+
+html = Tags()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/util/tests/__init__.py
@@ -0,0 +1,12 @@
+import unittest
+
+from trac.util.tests import markup, text
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(markup.suite())
+    suite.addTest(text.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/util/tests/markup.py
@@ -0,0 +1,190 @@
+# -*- encoding: utf-8 -*-
+
+import doctest
+from HTMLParser import HTMLParseError
+import unittest
+
+from trac.util.markup import escape, html, unescape, Element, Markup
+
+
+class MarkupTestCase(unittest.TestCase):
+
+    def test_escape(self):
+        markup = escape('<b>"&"</b>')
+        assert isinstance(markup, Markup)
+        self.assertEquals('&lt;b&gt;&#34;&amp;&#34;&lt;/b&gt;', markup)
+
+    def test_escape_noquotes(self):
+        markup = escape('<b>"&"</b>', quotes=False)
+        assert isinstance(markup, Markup)
+        self.assertEquals('&lt;b&gt;"&amp;"&lt;/b&gt;', markup)
+
+    def test_unescape_markup(self):
+        string = '<b>"&"</b>'
+        markup = Markup.escape(string)
+        assert isinstance(markup, Markup)
+        self.assertEquals(string, unescape(markup))
+
+    def test_add_str(self):
+        markup = Markup('<b>foo</b>') + '<br/>'
+        assert isinstance(markup, Markup)
+        self.assertEquals('<b>foo</b>&lt;br/&gt;', markup)
+
+    def test_add_markup(self):
+        markup = Markup('<b>foo</b>') + Markup('<br/>')
+        assert isinstance(markup, Markup)
+        self.assertEquals('<b>foo</b><br/>', markup)
+
+    def test_add_reverse(self):
+        markup = 'foo' + Markup('<b>bar</b>')
+        assert isinstance(markup, unicode)
+        self.assertEquals('foo<b>bar</b>', markup)
+
+    def test_mod(self):
+        markup = Markup('<b>%s</b>') % '&'
+        assert isinstance(markup, Markup)
+        self.assertEquals('<b>&amp;</b>', markup)
+
+    def test_mod_multi(self):
+        markup = Markup('<b>%s</b> %s') % ('&', 'boo')
+        assert isinstance(markup, Markup)
+        self.assertEquals('<b>&amp;</b> boo', markup)
+
+    def test_mul(self):
+        markup = Markup('<b>foo</b>') * 2
+        assert isinstance(markup, Markup)
+        self.assertEquals('<b>foo</b><b>foo</b>', markup)
+
+    def test_join(self):
+        markup = Markup('<br />').join(['foo', '<bar />', Markup('<baz />')])
+        assert isinstance(markup, Markup)
+        self.assertEquals('foo<br />&lt;bar /&gt;<br /><baz />', markup)
+
+    def test_stripentities_all(self):
+        markup = Markup('&amp; &#106;').stripentities()
+        assert isinstance(markup, Markup)
+        self.assertEquals('& j', markup)
+
+    def test_stripentities_keepxml(self):
+        markup = Markup('<a href="#">fo<br />o</a>').striptags()
+        assert isinstance(markup, Markup)
+        self.assertEquals('foo', markup)
+
+    def test_striptags_empty(self):
+        markup = Markup('<br />').striptags()
+        assert isinstance(markup, Markup)
+        self.assertEquals('', markup)
+
+    def test_striptags_mid(self):
+        markup = Markup('<a href="#">fo<br />o</a>').striptags()
+        assert isinstance(markup, Markup)
+        self.assertEquals('foo', markup)
+
+    def test_sanitize_unchanged(self):
+        markup = Markup('<a href="#">fo<br />o</a>')
+        self.assertEquals('<a href="#">fo<br />o</a>', markup.sanitize())
+
+    def test_sanitize_escape_text(self):
+        markup = Markup('<a href="#">fo&amp;</a>')
+        self.assertEquals('<a href="#">fo&amp;</a>', markup.sanitize())
+        markup = Markup('<a href="#">&lt;foo&gt;</a>')
+        self.assertEquals('<a href="#">&lt;foo&gt;</a>', markup.sanitize())
+
+    def test_sanitize_entityref_text(self):
+        markup = Markup('<a href="#">fo&ouml;</a>')
+        self.assertEquals(u'<a href="#">foö</a>', markup.sanitize())
+
+    def test_sanitize_escape_attr(self):
+        markup = Markup('<div title="&lt;foo&gt;"></div>')
+        self.assertEquals('<div title="&lt;foo&gt;"></div>', markup.sanitize())
+
+    def test_sanitize_close_empty_tag(self):
+        markup = Markup('<a href="#">fo<br>o</a>')
+        self.assertEquals('<a href="#">fo<br />o</a>', markup.sanitize())
+
+    def test_sanitize_invalid_entity(self):
+        markup = Markup('&junk;')
+        self.assertEquals('&amp;junk;', markup.sanitize())
+
+    def test_sanitize_remove_script_elem(self):
+        markup = Markup('<script>alert("Foo")</script>')
+        self.assertEquals('', markup.sanitize())
+        markup = Markup('<SCRIPT SRC="http://example.com/"></SCRIPT>')
+        self.assertEquals('', markup.sanitize())
+        markup = Markup('<SCR\0IPT>alert("foo")</SCR\0IPT>')
+        self.assertRaises(HTMLParseError, markup.sanitize)
+        markup = Markup('<SCRIPT&XYZ SRC="http://example.com/"></SCRIPT>')
+        self.assertRaises(HTMLParseError, markup.sanitize)
+
+    def test_sanitize_remove_onclick_attr(self):
+        markup = Markup('<div onclick=\'alert("foo")\' />')
+        self.assertEquals('<div></div>', markup.sanitize())
+
+    def test_sanitize_remove_style_scripts(self):
+        # Inline style with url() using javascript: scheme
+        markup = Markup('<DIV STYLE=\'background: url(javascript:alert("foo"))\'>')
+        self.assertEquals('<div>', markup.sanitize())
+        # Inline style with url() using javascript: scheme, using control char
+        markup = Markup('<DIV STYLE=\'background: url(&#1;javascript:alert("foo"))\'>')
+        self.assertEquals('<div>', markup.sanitize())
+        # Inline style with url() using javascript: scheme, in quotes
+        markup = Markup('<DIV STYLE=\'background: url("javascript:alert(foo)")\'>')
+        self.assertEquals('<div>', markup.sanitize())
+        # IE expressions in CSS not allowed
+        markup = Markup('<DIV STYLE=\'width: expression(alert("foo"));\'>')
+        self.assertEquals('<div>', markup.sanitize())
+        markup = Markup('<DIV STYLE=\'background: url(javascript:alert("foo"));'
+                                     'color: #fff\'>')
+        self.assertEquals('<div style="color: #fff">', markup.sanitize())
+
+    def test_sanitize_remove_src_javascript(self):
+        markup = Markup('<img src=\'javascript:alert("foo")\'>')
+        self.assertEquals('<img />', markup.sanitize())
+        # Case-insensitive protocol matching
+        markup = Markup('<IMG SRC=\'JaVaScRiPt:alert("foo")\'>')
+        self.assertEquals('<img />', markup.sanitize())
+        # Grave accents (not parsed)
+        markup = Markup('<IMG SRC=`javascript:alert("RSnake says, \'foo\'")`>')
+        self.assertRaises(HTMLParseError, markup.sanitize)
+        # Protocol encoded using UTF-8 numeric entities
+        markup = Markup('<IMG SRC=\'&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;'
+                        '&#112;&#116;&#58;alert("foo")\'>')
+        self.assertEquals('<img />', markup.sanitize())
+        # Protocol encoded using UTF-8 numeric entities without a semicolon
+        # (which is allowed because the max number of digits is used)
+        markup = Markup('<IMG SRC=\'&#0000106&#0000097&#0000118&#0000097'
+                        '&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116'
+                        '&#0000058alert("foo")\'>')
+        self.assertEquals('<img />', markup.sanitize())
+        # Protocol encoded using UTF-8 numeric hex entities without a semicolon
+        # (which is allowed because the max number of digits is used)
+        markup = Markup('<IMG SRC=\'&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69'
+                        '&#x70&#x74&#x3A;alert("foo")\'>')
+        self.assertEquals('<img />', markup.sanitize())
+        # Embedded tab character in protocol
+        markup = Markup('<IMG SRC=\'jav\tascript:alert("foo");\'>')
+        self.assertEquals('<img />', markup.sanitize())
+        # Embedded tab character in protocol, but encoded this time
+        markup = Markup('<IMG SRC=\'jav&#x09;ascript:alert("foo");\'>')
+        self.assertEquals('<img />', markup.sanitize())
+
+
+class TagsTestCase(unittest.TestCase):
+
+    def test_link(self):
+        link = html.A('Bar', href='#', title='Foo', accesskey=None)
+        bits = link.serialize()
+        self.assertEqual(u'<a href="#" title="Foo">', bits.next())
+        self.assertEqual(u'Bar', bits.next())
+        self.assertEqual(u'</a>', bits.next())
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(MarkupTestCase, 'test'))
+    suite.addTest(doctest.DocTestSuite(Element.__module__))
+    suite.addTest(unittest.makeSuite(TagsTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/util/tests/text.py
@@ -0,0 +1,72 @@
+# -*- encoding: utf-8 -*-
+
+import doctest
+import unittest
+
+from trac.util.text import to_unicode
+
+class ToUnicodeTestCase(unittest.TestCase):
+
+    def test_explicit(self):
+        uc = to_unicode('\xc3\xa7', 'utf-8')
+        assert isinstance(uc, unicode)
+        self.assertEquals(u'\xe7', uc)
+
+    def test_explicit_lossy(self):
+        uc = to_unicode('\xc3', 'utf-8')
+        assert isinstance(uc, unicode)
+        self.assertEquals(u'\ufffd', uc)
+
+    def test_explicit_lossless(self):
+        uc = to_unicode('\xc3', 'utf-8', lossy=False)
+        assert isinstance(uc, unicode)
+        self.assertEquals(u'\xc3', uc)
+
+    def test_implicit(self):
+        uc = to_unicode('\xc3\xa7')
+        assert isinstance(uc, unicode)
+        self.assertEquals(u'\xe7', uc)
+
+#     Note: the following test depends on the locale.getpreferredencoding()
+#
+#     def test_implicit_lossy(self):
+#         uc = to_unicode('\xc3')
+#         assert isinstance(uc, unicode)
+#         self.assertEquals(u'\xc3', uc)
+        
+    def test_implicit_lossless(self):
+        uc = to_unicode('\xc3', None, lossy=False)
+        assert isinstance(uc, unicode)
+        self.assertEquals(u'\xc3', uc)
+
+    def test_from_exception_using_unicode_args(self):
+        u = u'\uB144'
+        try:
+            raise ValueError, '%s is not a number.' % u
+        except ValueError, e:
+            self.assertEquals(u'\uB144 is not a number.', to_unicode(e))
+
+    def test_from_exception_using_str_args(self):
+        u = u'Das Ger\xe4t oder die Ressource ist belegt'
+        try:
+            raise ValueError, u.encode('utf-8')
+        except ValueError, e:
+            self.assertEquals(u, to_unicode(e))
+
+    def test_from_exception_using_str(self):
+        class PermissionError(StandardError):
+            def __str__(self):
+                return u'acc\xe8s interdit'
+        try:
+            raise PermissionError()
+        except PermissionError, e:
+            self.assertEquals(u'acc\xe8s interdit', to_unicode(e))
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(ToUnicodeTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/util/text.py
@@ -0,0 +1,152 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2006 Matthew Good <trac@matt-good.net>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Matthew Good <trac@matt-good.net>
+#         Christian Boos <cboos@neuf.fr>
+
+import locale
+import os
+import sys
+from urllib import quote, unquote, urlencode
+
+
+CRLF = '\r\n'
+
+# -- Unicode
+
+def to_unicode(text, charset=None, lossy=True):
+    """Convert a `str` object to an `unicode` object.
+
+    If `charset` is not specified, we'll make some guesses,
+    first trying the UTF-8 encoding then trying the locale
+    preferred encoding (this differs from the `unicode` function
+    which only tries with the locale preferred encoding, in 'strict'
+    mode).
+
+    If the `lossy` argument is `True`, which is the default, then
+    we use the 'replace' mode:
+
+    If the `lossy` argument is `False`, we fallback to the 'iso-8859-15'
+    charset in case of an error (encoding a `str` using 'iso-8859-15'
+    will always work, as there's one Unicode character for each byte of
+    the input).
+    """
+    if not isinstance(text, str):
+        if isinstance(text, Exception):
+            # two possibilities for storing unicode strings in exception data:
+            try:
+                # custom __str__ method on the exception (e.g. PermissionError)
+                return unicode(text)
+            except UnicodeError:
+                # unicode arguments given to the exception (e.g. parse_date)
+                return ' '.join([to_unicode(arg) for arg in text.args])
+        return unicode(text)
+    errors = lossy and 'replace' or 'strict'
+    try:
+        if charset:
+            return unicode(text, charset, errors)
+        else:
+            try:
+                return unicode(text, 'utf-8')
+            except UnicodeError:
+                return unicode(text, locale.getpreferredencoding(), errors)
+    except UnicodeError:
+        return unicode(text, 'iso-8859-15')
+
+def unicode_quote(value):
+    """A unicode aware version of urllib.quote"""
+    return quote(value.encode('utf-8'))
+
+def unicode_unquote(value):
+    """A unicode aware version of urllib.unquote.
+    
+    Take `str` value previously obtained by `unicode_quote`.
+    """
+    return unquote(value).decode('utf-8')
+
+def unicode_urlencode(params):
+    """A unicode aware version of urllib.urlencode"""
+    if isinstance(params, dict):
+        params = params.items()
+    return urlencode([(k, isinstance(v, unicode) and v.encode('utf-8') or v)
+                      for k, v in params])
+
+def to_utf8(text, charset='iso-8859-15'):
+    """Convert a string to UTF-8, assuming the encoding is either UTF-8, ISO
+    Latin-1, or as specified by the optional `charset` parameter.
+
+    ''Deprecated in 0.10. You should use `unicode` strings only.''
+    """
+    try:
+        # Do nothing if it's already utf-8
+        u = unicode(text, 'utf-8')
+        return text
+    except UnicodeError:
+        try:
+            # Use the user supplied charset if possible
+            u = unicode(text, charset)
+        except UnicodeError:
+            # This should always work
+            u = unicode(text, 'iso-8859-15')
+        return u.encode('utf-8')
+
+
+# -- Plain text formatting
+
+def shorten_line(text, maxlen=75):
+    if len(text or '') < maxlen:
+        return text
+    shortline = text[:maxlen]
+    cut = shortline.rfind(' ') + 1 or shortline.rfind('\n') + 1 or maxlen
+    shortline = text[:cut]+' ...'
+    return shortline
+
+def wrap(t, cols=75, initial_indent='', subsequent_indent='',
+         linesep=os.linesep):
+    try:
+        import textwrap
+        t = t.strip().replace('\r\n', '\n').replace('\r', '\n')
+        wrapper = textwrap.TextWrapper(cols, replace_whitespace=0,
+                                       break_long_words=0,
+                                       initial_indent=initial_indent,
+                                       subsequent_indent=subsequent_indent)
+        wrappedLines = []
+        for line in t.split('\n'):
+            wrappedLines += wrapper.wrap(line.rstrip()) or ['']
+        return linesep.join(wrappedLines)
+
+    except ImportError:
+        return t
+
+
+# -- Conversion
+
+def pretty_size(size):
+    if size is None:
+        return ''
+
+    jump = 512
+    if size < jump:
+        return '%d bytes' % size
+
+    units = ['kB', 'MB', 'GB', 'TB']
+    i = 0
+    while size >= jump and i < len(units):
+        i += 1
+        size /= 1024.
+
+    return '%.1f %s' % (size, units[i - 1])
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/__init__.py
@@ -0,0 +1,1 @@
+from trac.versioncontrol.api import *
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/api.py
@@ -0,0 +1,369 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+from heapq import heappop, heappush
+
+from trac.config import Option
+from trac.core import *
+from trac.perm import PermissionError
+
+
+class IRepositoryConnector(Interface):
+    """Extension point interface for components that provide support for a
+    specific version control system."""
+
+    def get_supported_types():
+        """Return the types of version control systems that are supported by
+        this connector, and their relative priorities.
+        
+        Highest number is highest priority.
+        """
+
+    def get_repository(repos_type, repos_dir, authname):
+        """Return the Repository object for the given repository type and
+        directory.
+        """
+
+
+class RepositoryManager(Component):
+    """Component that keeps track of the supported version control systems, and
+    provides easy access to the configured implementation."""
+
+    connectors = ExtensionPoint(IRepositoryConnector)
+
+    repository_type = Option('trac', 'repository_type', 'svn',
+        """Repository connector type. (''since 0.10'')""")
+    repository_dir = Option('trac', 'repository_dir', '',
+        """Path to local repository""")
+
+    def __init__(self):
+        self._connector = None
+
+    # Public API methods
+
+    def get_repository(self, authname):
+        if not self._connector:
+            candidates = []
+            for connector in self.connectors:
+                for repos_type_, prio in connector.get_supported_types():
+                    if self.repository_type != repos_type_:
+                        continue
+                    heappush(candidates, (-prio, connector))
+            if not candidates:
+                raise TracError, 'Unsupported version control system "%s"' \
+                                 % self.repository_type
+            self._connector = heappop(candidates)[1]
+        return self._connector.get_repository(self.repository_type,
+                                              self.repository_dir, authname)
+
+
+class NoSuchChangeset(TracError):
+    def __init__(self, rev):
+        TracError.__init__(self, "No changeset %s in the repository" % rev)
+
+class NoSuchNode(TracError):
+    def __init__(self, path, rev, msg=None):
+        TracError.__init__(self, "%sNo node %s at revision %s" \
+                           % (msg and '%s: ' % msg or '', path, rev))
+
+class Repository(object):
+    """
+    Base class for a repository provided by a version control system.
+    """
+
+    def __init__(self, name, authz, log):
+        self.name = name
+        self.authz = authz or Authorizer()
+        self.log = log
+
+    def close(self):
+        """
+        Close the connection to the repository.
+        """
+        raise NotImplementedError
+
+    def get_changeset(self, rev):
+        """
+        Retrieve a Changeset object that describes the changes made in
+        revision 'rev'.
+        """
+        raise NotImplementedError
+
+    def get_changesets(self, start, stop):
+        """
+        Generate Changeset belonging to the given time period (start, stop).
+        """
+        rev = self.youngest_rev
+        while rev:
+            if self.authz.has_permission_for_changeset(rev):
+                chgset = self.get_changeset(rev)
+                if chgset.date < start:
+                    return
+                if chgset.date < stop:
+                    yield chgset
+            rev = self.previous_rev(rev)
+
+    def has_node(self, path, rev=None):
+        """
+        Tell if there's a node at the specified (path,rev) combination.
+
+        When `rev` is `None`, the latest revision is implied.
+        """
+        try:
+            self.get_node(path, rev)
+            return True
+        except TracError:
+            return False        
+    
+    def get_node(self, path, rev=None):
+        """
+        Retrieve a Node (directory or file) from the repository at the
+        given path. If the rev parameter is specified, the version of the
+        node at that revision is returned, otherwise the latest version
+        of the node is returned.
+        """
+        raise NotImplementedError
+
+    def get_oldest_rev(self):
+        """
+        Return the oldest revision stored in the repository.
+        """
+        raise NotImplementedError
+    oldest_rev = property(lambda x: x.get_oldest_rev())
+
+    def get_youngest_rev(self):
+        """
+        Return the youngest revision in the repository.
+        """
+        raise NotImplementedError
+    youngest_rev = property(lambda x: x.get_youngest_rev())
+
+    def previous_rev(self, rev):
+        """
+        Return the revision immediately preceding the specified revision.
+        """
+        raise NotImplementedError
+
+    def next_rev(self, rev, path=''):
+        """
+        Return the revision immediately following the specified revision.
+        """
+        raise NotImplementedError
+
+    def rev_older_than(self, rev1, rev2):
+        """
+        Return True if rev1 is older than rev2, i.e. if rev1 comes before rev2
+        in the revision sequence.
+        """
+        raise NotImplementedError
+
+    def get_youngest_rev_in_cache(self, db):
+        """
+        Return the youngest revision currently cached.
+        The way revisions are sequenced is version control specific.
+        By default, one assumes that the revisions are sequenced in time.
+        """
+        cursor = db.cursor()
+        cursor.execute("SELECT rev FROM revision ORDER BY time DESC LIMIT 1")
+        row = cursor.fetchone()
+        return row and row[0] or None
+
+    def get_path_history(self, path, rev=None, limit=None):
+        """
+        Retrieve all the revisions containing this path (no newer than 'rev').
+        The result format should be the same as the one of Node.get_history()
+        """
+        raise NotImplementedError
+
+    def normalize_path(self, path):
+        """
+        Return a canonical representation of path in the repos.
+        """
+        return NotImplementedError
+
+    def normalize_rev(self, rev):
+        """
+        Return a canonical representation of a revision in the repos.
+        'None' is a valid revision value and represents the youngest revision.
+        """
+        return NotImplementedError
+
+    def short_rev(self, rev):
+        """
+        Return a compact representation of a revision in the repos.
+        """
+        return self.normalize_rev(rev)
+        
+    def get_changes(self, old_path, old_rev, new_path, new_rev,
+                    ignore_ancestry=1):
+        """
+        Generator that yields change tuples (old_node, new_node, kind, change)
+        for each node change between the two arbitrary (path,rev) pairs.
+
+        The old_node is assumed to be None when the change is an ADD,
+        the new_node is assumed to be None when the change is a DELETE.
+        """
+        raise NotImplementedError
+
+
+class Node(object):
+    """
+    Represents a directory or file in the repository.
+    """
+
+    DIRECTORY = "dir"
+    FILE = "file"
+
+    def __init__(self, path, rev, kind):
+        assert kind in (Node.DIRECTORY, Node.FILE), "Unknown node kind %s" % kind
+        self.path = unicode(path)
+        self.rev = rev
+        self.kind = kind
+
+    def get_content(self):
+        """
+        Return a stream for reading the content of the node. This method
+        will return None for directories. The returned object should provide
+        a read([len]) function.
+        """
+        raise NotImplementedError
+
+    def get_entries(self):
+        """
+        Generator that yields the immediate child entries of a directory, in no
+        particular order. If the node is a file, this method returns None.
+        """
+        raise NotImplementedError
+
+    def get_history(self, limit=None):
+        """
+        Generator that yields (path, rev, chg) tuples, one for each revision in which
+        the node was changed. This generator will follow copies and moves of a
+        node (if the underlying version control system supports that), which
+        will be indicated by the first element of the tuple (i.e. the path)
+        changing.
+        Starts with an entry for the current revision.
+        """
+        raise NotImplementedError
+
+    def get_previous(self):
+        """
+        Return the (path, rev, chg) tuple corresponding to the previous
+        revision for that node.
+        """
+        skip = True
+        for p in self.get_history(2):
+            if skip:
+                skip = False
+            else:
+                return p
+
+    def get_properties(self):
+        """
+        Returns a dictionary containing the properties (meta-data) of the node.
+        The set of properties depends on the version control system.
+        """
+        raise NotImplementedError
+
+    def get_content_length(self):
+        raise NotImplementedError
+    content_length = property(lambda x: x.get_content_length())
+
+    def get_content_type(self):
+        raise NotImplementedError
+    content_type = property(lambda x: x.get_content_type())
+
+    def get_name(self):
+        return self.path.split('/')[-1]
+    name = property(lambda x: x.get_name())
+
+    def get_last_modified(self):
+        raise NotImplementedError
+    last_modified = property(lambda x: x.get_last_modified())
+
+    isdir = property(lambda x: x.kind == Node.DIRECTORY)
+    isfile = property(lambda x: x.kind == Node.FILE)
+
+
+class Changeset(object):
+    """
+    Represents a set of changes of a repository.
+    """
+
+    ADD = 'add'
+    COPY = 'copy'
+    DELETE = 'delete'
+    EDIT = 'edit'
+    MOVE = 'move'
+
+    def __init__(self, rev, message, author, date):
+        self.rev = rev
+        self.message = message
+        self.author = author
+        self.date = date
+
+    def get_properties(self):
+        """Generator that provide additional metadata for this changeset.
+
+        Each additional property is a 4 element tuple:
+         * `name` is the name of the property,
+         * `text` its value
+         * `wikiflag` indicates whether the `text` should be interpreted as
+            wiki text or not
+         * `htmlclass` enables to attach special formatting to the displayed
+            property, e.g. `'author'`, `'time'`, `'message'` or `'changeset'`.
+        """
+        
+    def get_changes(self):
+        """
+        Generator that produces a (path, kind, change, base_path, base_rev)
+        tuple for every change in the changeset, where change can be one of
+        Changeset.ADD, Changeset.COPY, Changeset.DELETE, Changeset.EDIT or
+        Changeset.MOVE, and kind is one of Node.FILE or Node.DIRECTORY.
+        """
+        raise NotImplementedError
+
+
+class PermissionDenied(PermissionError):
+    """
+    Exception raised by an authorizer if the user has insufficient permissions
+    to view a specific part of the repository.
+    """
+    def __str__(self):
+        return self.action
+
+
+class Authorizer(object):
+    """
+    Base class for authorizers that are responsible to granting or denying
+    access to view certain parts of a repository.
+    """
+
+    def assert_permission(self, path):
+        if not self.has_permission(path):
+            raise PermissionDenied, \
+                  'Insufficient permissions to access %s' % path
+
+    def assert_permission_for_changeset(self, rev):
+        if not self.has_permission_for_changeset(rev):
+            raise PermissionDenied, \
+                  'Insufficient permissions to access changeset %s' % rev
+
+    def has_permission(self, path):
+        return True
+
+    def has_permission_for_changeset(self, rev):
+        return True
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/cache.py
@@ -0,0 +1,176 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+from trac.core import TracError
+from trac.versioncontrol import Changeset, Node, Repository, Authorizer, \
+                                NoSuchChangeset
+
+
+_kindmap = {'D': Node.DIRECTORY, 'F': Node.FILE}
+_actionmap = {'A': Changeset.ADD, 'C': Changeset.COPY,
+              'D': Changeset.DELETE, 'E': Changeset.EDIT,
+              'M': Changeset.MOVE}
+
+
+class CachedRepository(Repository):
+
+    def __init__(self, db, repos, authz, log):
+        Repository.__init__(self, repos.name, authz, log)
+        self.db = db
+        self.repos = repos
+        self.synced = 0
+
+    def close(self):
+        self.repos.close()
+
+    def get_changeset(self, rev):
+        if not self.synced:
+            self.sync()
+            self.synced = 1
+        return CachedChangeset(self.repos.normalize_rev(rev), self.db,
+                               self.authz)
+
+    def get_changesets(self, start, stop):
+        if not self.synced:
+            self.sync()
+            self.synced = 1
+        cursor = self.db.cursor()
+        cursor.execute("SELECT rev FROM revision "
+                       "WHERE time >= %s AND time < %s "
+                       "ORDER BY time", (start, stop))
+        for rev, in cursor:
+            if self.authz.has_permission_for_changeset(rev):
+                yield self.get_changeset(rev)
+
+    def sync(self):
+        self.log.debug("Checking whether sync with repository is needed")
+        cursor = self.db.cursor()
+
+        # -- repository used for populating the cache
+        cursor.execute("SELECT value FROM system WHERE name='repository_dir'")
+        row = cursor.fetchone()
+        if row:
+            previous_repository_dir = row[0]
+        else: # no 'repository_dir' stored yet, assume everything's OK
+            previous_repository_dir = self.name
+
+        if self.name != previous_repository_dir:
+            raise TracError, ("The 'repository_dir' has changed, "
+                              "a 'trac-admin resync' operation is needed.")
+
+        youngest_stored = self.repos.get_youngest_rev_in_cache(self.db)
+
+        if youngest_stored != str(self.repos.youngest_rev):
+            authz = self.repos.authz
+            self.repos.authz = Authorizer() # remove permission checking
+
+            kindmap = dict(zip(_kindmap.values(), _kindmap.keys()))
+            actionmap = dict(zip(_actionmap.values(), _actionmap.keys()))
+            self.log.info("Syncing with repository (%s to %s)"
+                          % (youngest_stored, self.repos.youngest_rev))
+            if youngest_stored:
+                current_rev = self.repos.next_rev(youngest_stored)
+            else:
+                try:
+                    current_rev = self.repos.oldest_rev
+                    current_rev = self.repos.normalize_rev(current_rev)
+                except TracError:
+                    current_rev = None
+            while current_rev is not None:
+                changeset = self.repos.get_changeset(current_rev)
+                cursor.execute("INSERT INTO revision (rev,time,author,message) "
+                               "VALUES (%s,%s,%s,%s)", (str(current_rev),
+                               changeset.date, changeset.author,
+                               changeset.message))
+                for path,kind,action,base_path,base_rev in changeset.get_changes():
+                    self.log.debug("Caching node change in [%s]: %s"
+                                   % (current_rev, (path, kind, action,
+                                      base_path, base_rev)))
+                    kind = kindmap[kind]
+                    action = actionmap[action]
+                    cursor.execute("INSERT INTO node_change (rev,path,"
+                                   "node_type,change_type,base_path,base_rev) "
+                                   "VALUES (%s,%s,%s,%s,%s,%s)",
+                                   (str(current_rev), path, kind, action,
+                                   base_path, base_rev))
+                current_rev = self.repos.next_rev(current_rev)
+            self.db.commit()
+            self.repos.authz = authz # restore permission checking
+
+    def get_node(self, path, rev=None):
+        return self.repos.get_node(path, rev)
+
+    def has_node(self, path, rev):
+        return self.repos.has_node(path, rev)
+
+    def get_oldest_rev(self):
+        return self.repos.oldest_rev
+
+    def get_youngest_rev(self):
+        return self.repos.youngest_rev
+
+    def previous_rev(self, rev):
+        return self.repos.previous_rev(rev)
+
+    def next_rev(self, rev, path=''):
+        return self.repos.next_rev(rev, path)
+
+    def rev_older_than(self, rev1, rev2):
+        return self.repos.rev_older_than(rev1, rev2)
+
+    def get_path_history(self, path, rev=None, limit=None):
+        return self.repos.get_path_history(path, rev, limit)
+
+    def normalize_path(self, path):
+        return self.repos.normalize_path(path)
+
+    def normalize_rev(self, rev):
+        return self.repos.normalize_rev(rev)
+
+    def get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1):
+        return self.repos.get_changes(old_path, old_rev, new_path, new_rev, ignore_ancestry)
+
+
+class CachedChangeset(Changeset):
+
+    def __init__(self, rev, db, authz):
+        self.db = db
+        self.authz = authz
+        cursor = self.db.cursor()
+        cursor.execute("SELECT time,author,message FROM revision "
+                       "WHERE rev=%s", (rev,))
+        row = cursor.fetchone()
+        if row:
+            date, author, message = row
+            Changeset.__init__(self, rev, message, author, int(date))
+        else:
+            raise NoSuchChangeset(rev)
+
+    def get_changes(self):
+        cursor = self.db.cursor()
+        cursor.execute("SELECT path,node_type,change_type,base_path,base_rev "
+                       "FROM node_change WHERE rev=%s "
+                       "ORDER BY path", (self.rev,))
+        for path, kind, change, base_path, base_rev in cursor:
+            if not self.authz.has_permission(path):
+                # FIXME: what about the base_path?
+                continue
+            kind = _kindmap[kind]
+            change = _actionmap[change]
+            yield path, kind, change, base_path, base_rev
+
+    def get_properties(self):
+        return []
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/diff.py
@@ -0,0 +1,278 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2006 Edgewall Software
+# Copyright (C) 2004-2006 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+from trac.util.markup import escape, Markup
+
+from difflib import SequenceMatcher
+import re
+
+__all__ = ['get_diff_options', 'hdf_diff', 'unified_diff']
+
+
+def _get_change_extent(str1, str2):
+    """
+    Determines the extent of differences between two strings. Returns a tuple
+    containing the offset at which the changes start, and the negative offset
+    at which the changes end. If the two strings have neither a common prefix
+    nor a common suffix, (0, 0) is returned.
+    """
+    start = 0
+    limit = min(len(str1), len(str2))
+    while start < limit and str1[start] == str2[start]:
+        start += 1
+    end = -1
+    limit = limit - start
+    while -end <= limit and str1[end] == str2[end]:
+        end -= 1
+    return (start, end + 1)
+
+def _get_opcodes(fromlines, tolines, ignore_blank_lines=False,
+                 ignore_case=False, ignore_space_changes=False):
+    """
+    Generator built on top of SequenceMatcher.get_opcodes().
+    
+    This function detects line changes that should be ignored and emits them
+    as tagged as 'equal', possibly joined with the preceding and/or following
+    'equal' block.
+    """
+
+    def is_ignorable(tag, fromlines, tolines):
+        if tag == 'delete' and ignore_blank_lines:
+            if ''.join(fromlines) == '':
+                return True
+        elif tag == 'insert' and ignore_blank_lines:
+            if ''.join(tolines) == '':
+                return True
+        elif tag == 'replace' and (ignore_case or ignore_space_changes):
+            if len(fromlines) != len(tolines):
+                return False
+            def f(str):
+                if ignore_case:
+                    str = str.lower()
+                if ignore_space_changes:
+                    str = ' '.join(str.split())
+                return str
+            for i in range(len(fromlines)):
+                if f(fromlines[i]) != f(tolines[i]):
+                    return False
+            return True
+
+    matcher = SequenceMatcher(None, fromlines, tolines)
+    previous = None
+    for tag, i1, i2, j1, j2 in matcher.get_opcodes():
+        if tag == 'equal':
+            if previous:
+                previous = (tag, previous[1], i2, previous[3], j2)
+            else:
+                previous = (tag, i1, i2, j1, j2)
+        else:
+            if is_ignorable(tag, fromlines[i1:i2], tolines[j1:j2]):
+                if previous:
+                    previous = 'equal', previous[1], i2, previous[3], j2
+                else:
+                    previous = 'equal', i1, i2, j1, j2
+                continue
+            if previous:
+                yield previous
+            yield tag, i1, i2, j1, j2
+            previous = None
+
+    if previous:
+        yield previous
+
+def _group_opcodes(opcodes, n=3):
+    """
+    Python 2.2 doesn't have SequenceMatcher.get_grouped_opcodes(), so let's
+    provide equivalent here. The opcodes parameter can be any iterable or
+    sequence.
+
+    This function can also be used to generate full-context diffs by passing 
+    None for the parameter n.
+    """
+    # Full context produces all the opcodes
+    if n is None:
+        yield opcodes
+        return
+
+    # Otherwise we leave at most n lines with the tag 'equal' before and after
+    # every change
+    nn = n + n
+    group = []
+    for idx, (tag, i1, i2, j1, j2) in enumerate(opcodes):
+        if idx == 0 and tag == 'equal': # Fixup leading unchanged block
+            i1, j1 = max(i1, i2 - n), max(j1, j2 - n)
+        elif tag == 'equal' and i2 - i1 > nn:
+            group.append((tag, i1, min(i2, i1 + n), j1, min(j2, j1 + n)))
+            yield group
+            group = []
+            i1, j1 = max(i1, i2 - n), max(j1, j2 - n)
+        group.append((tag, i1, i2, j1 ,j2))
+
+    if group and not (len(group) == 1 and group[0][0] == 'equal'):
+        if group[-1][0] == 'equal': # Fixup trailing unchanged block
+            tag, i1, i2, j1, j2 = group[-1]
+            group[-1] = tag, i1, min(i2, i1 + n), j1, min(j2, j1 + n)
+        yield group
+
+def hdf_diff(fromlines, tolines, context=None, tabwidth=8,
+             ignore_blank_lines=0, ignore_case=0, ignore_space_changes=0):
+    """
+    Return an array that is adequate for adding the the HDF data set for HTML
+    rendering of the differences.
+    """
+
+    type_map = {'replace': 'mod', 'delete': 'rem', 'insert': 'add',
+                'equal': 'unmod'}
+
+    space_re = re.compile(' ( +)|^ ')
+    def htmlify(match):
+        div, mod = divmod(len(match.group(0)), 2)
+        return div * '&nbsp; ' + mod * '&nbsp;'
+
+    def markup_intraline_changes(opcodes):
+        for tag, i1, i2, j1, j2 in opcodes:
+            if tag == 'replace' and i2 - i1 == j2 - j1:
+                for i in range(i2 - i1):
+                    fromline, toline = fromlines[i1 + i], tolines[j1 + i]
+                    (start, end) = _get_change_extent(fromline, toline)
+
+                    if start == 0 and end < 0:
+                        # Change at start of line
+                        fromlines[i1 + i] = '\0' + fromline[:end] + '\1' + \
+                                            fromline[end:]
+                        tolines[j1 + i] = '\0' + toline[:end] + '\1' + \
+                                          toline[end:]
+                    elif start > 0 and end == 0:
+                        # Change at end of line
+                        fromlines[i1 + i] = fromline[:start] + '\0' + \
+                                            fromline[start:] + '\1'
+                        tolines[j1 + i] = toline[:start] + '\0' + \
+                                          toline[start:] + '\1'
+                    elif start > 0 and end < 0:
+                        # Change somewhere in the middle
+                        fromlines[i1 + i] = fromline[:start] + '\0' + \
+                                            fromline[start:end] + '\1' + \
+                                            fromline[end:]
+                        tolines[j1 + i] = toline[:start] + '\0' + \
+                                          toline[start:end] + '\1' + \
+                                          toline[end:]
+            yield tag, i1, i2, j1, j2
+
+    changes = []
+    opcodes = _get_opcodes(fromlines, tolines, ignore_blank_lines, ignore_case,
+                           ignore_space_changes)
+    for group in _group_opcodes(opcodes, context):
+        blocks = []
+        last_tag = None
+        for tag, i1, i2, j1, j2 in markup_intraline_changes(group):
+            if tag != last_tag:
+                blocks.append({'type': type_map[tag], 'base.offset': i1,
+                               'base.lines': [], 'changed.offset': j1,
+                               'changed.lines': []})
+            if tag == 'equal':
+                for line in fromlines[i1:i2]:
+                    line = line.expandtabs(tabwidth)
+                    line = space_re.sub(htmlify, escape(line, quotes=False))
+                    blocks[-1]['base.lines'].append(Markup(line))
+                for line in tolines[j1:j2]:
+                    line = line.expandtabs(tabwidth)
+                    line = space_re.sub(htmlify, escape(line, quotes=False))
+                    blocks[-1]['changed.lines'].append(Markup(line))
+            else:
+                if tag in ('replace', 'delete'):
+                    for line in fromlines[i1:i2]:
+                        line = line.expandtabs(tabwidth)
+                        line = escape(line, quotes=False).replace('\0', '<del>') \
+                                                         .replace('\1', '</del>')
+                        blocks[-1]['base.lines'].append(Markup(space_re.sub(htmlify,
+                                                                            line)))
+                if tag in ('replace', 'insert'):
+                    for line in tolines[j1:j2]:
+                        line = line.expandtabs(tabwidth)
+                        line = escape(line, quotes=False).replace('\0', '<ins>') \
+                                                         .replace('\1', '</ins>')
+                        blocks[-1]['changed.lines'].append(Markup(space_re.sub(htmlify,
+                                                                               line)))
+        changes.append(blocks)
+    return changes
+
+def unified_diff(fromlines, tolines, context=None, ignore_blank_lines=0,
+                 ignore_case=0, ignore_space_changes=0):
+    opcodes = _get_opcodes(fromlines, tolines, ignore_blank_lines, ignore_case,
+                           ignore_space_changes)
+    for group in _group_opcodes(opcodes, context):
+        i1, i2, j1, j2 = group[0][1], group[-1][2], group[0][3], group[-1][4]
+        if i1 == 0 and i2 == 0:
+            i1, i2 = -1, -1 # support for 'A'dd changes
+        yield '@@ -%d,%d +%d,%d @@' % (i1 + 1, i2 - i1, j1 + 1, j2 - j1)
+        for tag, i1, i2, j1, j2 in group:
+            if tag == 'equal':
+                for line in fromlines[i1:i2]:
+                    yield ' ' + line
+            else:
+                if tag in ('replace', 'delete'):
+                    for line in fromlines[i1:i2]:
+                        yield '-' + line
+                if tag in ('replace', 'insert'):
+                    for line in tolines[j1:j2]:
+                        yield '+' + line
+
+def get_diff_options(req):
+
+    def get_bool_option(name, default=0):
+        pref = int(req.session.get('diff_' + name, default))
+        arg = int(req.args.has_key(name))
+        if req.args.has_key('update') and arg != pref:
+            req.session['diff_' + name] = arg
+        else:
+            arg = pref
+        return arg
+
+    pref = req.session.get('diff_style', 'inline')
+    style = req.args.get('style', pref)
+    if req.args.has_key('update') and style != pref:
+        req.session['diff_style'] = style
+    req.hdf['diff.style'] = style
+
+    pref = int(req.session.get('diff_contextlines', 2))
+    try:
+        arg = int(req.args.get('contextlines', pref))
+    except ValueError:
+        arg = -1
+    if req.args.has_key('update') and arg != pref:
+        req.session['diff_contextlines'] = arg
+    options = ['-U%d' % arg]
+    if arg >= 0:
+        req.hdf['diff.options.contextlines'] = arg
+    else:
+        req.hdf['diff.options.contextlines'] = 'all'
+
+    arg = get_bool_option('ignoreblanklines')
+    if arg:
+        options.append('-B')
+    req.hdf['diff.options.ignoreblanklines'] = arg
+
+    arg = get_bool_option('ignorecase')
+    if arg:
+        options.append('-i')
+    req.hdf['diff.options.ignorecase'] = arg
+
+    arg = get_bool_option('ignorewhitespace')
+    if arg:
+        options.append('-b')
+    req.hdf['diff.options.ignorewhitespace'] = arg
+
+    return (style, options)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/svn_authz.py
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2005 Edgewall Software
+# Copyright (C) 2004 Francois Harvey <fharvey@securiweb.net>
+# Copyright (C) 2005 Matthew Good <trac@matt-good.net>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Francois Harvey <fharvey@securiweb.net>
+#         Matthew Good <trac@matt-good.net>
+
+from trac.config import Option
+from trac.core import *
+from trac.versioncontrol import Authorizer
+
+
+class SvnAuthzOptions(Component):
+
+    authz_file = Option('trac', 'authz_file', '',
+        """Path to Subversion
+        [http://svnbook.red-bean.com/en/1.1/ch06s04.html#svn-ch-6-sect-4.4.2 authorization (authz) file]
+        """)
+
+    authz_module_name = Option('trac', 'authz_module_name', '',
+        """The module prefix used in the authz_file.""")
+
+
+def SubversionAuthorizer(env, authname):
+    authz_file = env.config.get('trac', 'authz_file')
+    if not authz_file:
+        return Authorizer()
+
+    module_name = env.config.get('trac', 'authz_module_name')
+    db = env.get_db_cnx()
+    return RealSubversionAuthorizer(db, authname, module_name, authz_file)
+
+def parent_iter(path):
+    path = path.strip('/')
+    if path:
+        path = '/' + path + '/'
+    else:
+        path = '/'
+
+    while 1:
+        yield path
+        if path == '/':
+            raise StopIteration()
+        path = path[:-1]
+        yield path
+        idx = path.rfind('/')
+        path = path[:idx + 1]
+
+
+class RealSubversionAuthorizer(Authorizer):
+
+    auth_name = ''
+    module_name = ''
+    conf_authz = None
+
+    def __init__(self, db, auth_name, module_name, cfg_file, cfg_fp=None):
+        self.db = db
+        self.auth_name = auth_name
+        self.module_name = module_name
+                                
+        from ConfigParser import ConfigParser
+        self.conf_authz = ConfigParser()
+        if cfg_fp:
+            self.conf_authz.readfp(cfg_fp, cfg_file)
+        elif cfg_file:
+            self.conf_authz.read(cfg_file)
+
+        self.groups = self._groups()
+
+    def has_permission(self, path):
+        if path is None:
+            return 1
+
+        for p in parent_iter(path):
+            if self.module_name:
+                for perm in self._get_section(self.module_name + ':' + p):
+                    if perm is not None:
+                        return perm
+            for perm in self._get_section(p):
+                if perm is not None:
+                    return perm
+
+        return 0
+
+    def has_permission_for_changeset(self, rev):
+        cursor = self.db.cursor()
+        cursor.execute("SELECT path FROM node_change WHERE rev=%s", (rev,))
+        for row in cursor:
+            if self.has_permission(row[0]):
+                return 1
+        return 0
+
+    # Internal API
+
+    def _groups(self):
+        if not self.conf_authz.has_section('groups'):
+            return []
+
+        grp_parents = {}
+        usr_grps = []
+
+        for group in self.conf_authz.options('groups'):
+            for member in self.conf_authz.get('groups', group).split(','):
+                member = member.strip()
+                if member == self.auth_name:
+                    usr_grps.append(group)
+                elif member.startswith('@'):
+                    grp_parents.setdefault(member[1:], []).append(group)
+
+        expanded = {}
+
+        def expand_group(group):
+            if group in expanded:
+                return
+            expanded[group] = True
+            for parent in grp_parents.get(group, []):
+                expand_group(parent)
+
+        for g in usr_grps:
+            expand_group(g)
+
+        # expand groups
+        return expanded.keys()
+
+    def _get_section(self, section):
+        if not self.conf_authz.has_section(section):
+            return
+
+        yield self._get_permission(section, self.auth_name)
+
+        group_perm = None
+        for g in self.groups:
+            p = self._get_permission(section, '@' + g)
+            if p is not None:
+                group_perm = p
+
+            if group_perm:
+                yield 1
+
+        yield group_perm
+
+        yield self._get_permission(section, '*')
+
+    def _get_permission(self, section, subject):
+        if self.conf_authz.has_option(section, subject):
+            return 'r' in self.conf_authz.get(section, subject)
+        return None
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/svn_fs.py
@@ -0,0 +1,764 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2006 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+#         Christian Boos <cboos@neuf.fr>
+
+"""
+Note about Unicode:
+  All paths (or strings) manipulated by the Subversion bindings are
+  assumed to be UTF-8 encoded.
+
+  All paths manipulated by Trac are `unicode` objects.
+
+  Therefore:
+   * before being handed out to SVN, the Trac paths have to be encoded to UTF-8,
+     using `_to_svn()`
+   * before being handed out to Trac, a SVN path has to be decoded from UTF-8,
+     using `_from_svn()`
+
+  Warning: `SubversionNode.get_content` returns an object from which one
+           can read a stream of bytes.
+           NO guarantees can be given about what that stream of bytes
+           represents.
+           It might be some text, encoded in some way or another.
+           SVN properties __might__ give some hints about the content,
+           but they actually only reflect the beliefs of whomever set
+           those properties...
+"""
+
+import os.path
+import time
+import weakref
+import posixpath
+
+from trac.core import *
+from trac.versioncontrol import Changeset, Node, Repository, \
+                                IRepositoryConnector, \
+                                NoSuchChangeset, NoSuchNode
+from trac.versioncontrol.cache import CachedRepository
+from trac.versioncontrol.svn_authz import SubversionAuthorizer
+from trac.util.text import to_unicode
+
+try:
+    from svn import fs, repos, core, delta
+    has_subversion = True
+except ImportError:
+    has_subversion = False
+    class dummy_svn(object):
+        svn_node_dir = 1
+        svn_node_file = 2
+        def apr_pool_destroy(): pass
+        def apr_terminate(): pass
+        def apr_pool_clear(): pass
+        Editor = object
+    delta = core = dummy_svn()
+    
+
+_kindmap = {core.svn_node_dir: Node.DIRECTORY,
+            core.svn_node_file: Node.FILE}
+
+
+application_pool = None
+    
+def _get_history(svn_path, authz, fs_ptr, pool, start, end, limit=None):
+    """`svn_path` is assumed to be a UTF-8 encoded string.
+    Returned history paths will be `unicode` objects though."""
+    history = []
+    if hasattr(repos, 'svn_repos_history2'):
+        # For Subversion >= 1.1
+        def authz_cb(root, path, pool):
+            if limit and len(history) >= limit:
+                return 0
+            return authz.has_permission(_from_svn(path)) and 1 or 0
+        def history2_cb(path, rev, pool):
+            history.append((_from_svn(path), rev))
+        repos.svn_repos_history2(fs_ptr, svn_path, history2_cb, authz_cb,
+                                 start, end, 1, pool())
+    else:
+        # For Subversion 1.0.x
+        def history_cb(path, rev, pool):
+            path = _from_svn(path)
+            if authz.has_permission(path):
+                history.append((path, rev))
+        repos.svn_repos_history(fs_ptr, svn_path, history_cb,
+                                start, end, 1, pool())
+    for item in history:
+        yield item
+
+def _to_svn(*args):
+    """Expect a list of `unicode` path components.
+    Returns an UTF-8 encoded string suitable for the Subversion python bindings.
+    """
+    return '/'.join([path.strip('/') for path in args]).encode('utf-8')
+    
+def _from_svn(path):
+    """Expect an UTF-8 encoded string and transform it to an `unicode` object"""
+    return path and path.decode('utf-8')
+    
+def _normalize_path(path):
+    """Remove leading "/", except for the root."""
+    return path and path.strip('/') or '/'
+
+def _path_within_scope(scope, fullpath):
+    """Remove the leading scope from repository paths.
+
+    Return `None` if the path is not is scope.
+    """
+    if fullpath is not None:
+        fullpath = fullpath.lstrip('/')
+        if scope == '/':
+            return _normalize_path(fullpath)
+        scope = scope.strip('/')
+        if (fullpath + '/').startswith(scope + '/'):
+            return fullpath[len(scope) + 1:] or '/'
+
+def _is_path_within_scope(scope, fullpath):
+    """Check whether the given `fullpath` is within the given `scope`"""
+    if scope == '/':
+        return fullpath is not None
+    fullpath = fullpath and fullpath.lstrip('/') or ''
+    scope = scope.strip('/')
+    return (fullpath + '/').startswith(scope + '/')
+
+
+def _mark_weakpool_invalid(weakpool):
+    if weakpool():
+        weakpool()._mark_invalid()
+
+
+class Pool(object):
+    """A Pythonic memory pool object"""
+
+    # Protect svn.core methods from GC
+    apr_pool_destroy = staticmethod(core.apr_pool_destroy)
+    apr_terminate = staticmethod(core.apr_terminate)
+    apr_pool_clear = staticmethod(core.apr_pool_clear)
+    
+    def __init__(self, parent_pool=None):
+        """Create a new memory pool"""
+
+        global application_pool
+        self._parent_pool = parent_pool or application_pool
+
+        # Create pool
+        if self._parent_pool:
+            self._pool = core.svn_pool_create(self._parent_pool())
+        else:
+            # If we are an application-level pool,
+            # then initialize APR and set this pool
+            # to be the application-level pool
+            core.apr_initialize()
+            application_pool = self
+
+            self._pool = core.svn_pool_create(None)
+        self._mark_valid()
+
+    def __call__(self):
+        return self._pool
+
+    def valid(self):
+        """Check whether this memory pool and its parents
+        are still valid"""
+        return hasattr(self,"_is_valid")
+
+    def assert_valid(self):
+        """Assert that this memory_pool is still valid."""
+        assert self.valid();
+
+    def clear(self):
+        """Clear embedded memory pool. Invalidate all subpools."""
+        self.apr_pool_clear(self._pool)
+        self._mark_valid()
+
+    def destroy(self):
+        """Destroy embedded memory pool. If you do not destroy
+        the memory pool manually, Python will destroy it
+        automatically."""
+
+        global application_pool
+
+        self.assert_valid()
+
+        # Destroy pool
+        self.apr_pool_destroy(self._pool)
+
+        # Clear application pool and terminate APR if necessary
+        if not self._parent_pool:
+            application_pool = None
+            self.apr_terminate()
+
+        self._mark_invalid()
+
+    def __del__(self):
+        """Automatically destroy memory pools, if necessary"""
+        if self.valid():
+            self.destroy()
+
+    def _mark_valid(self):
+        """Mark pool as valid"""
+        if self._parent_pool:
+            # Refer to self using a weakreference so that we don't
+            # create a reference cycle
+            weakself = weakref.ref(self)
+
+            # Set up callbacks to mark pool as invalid when parents
+            # are destroyed
+            self._weakref = weakref.ref(self._parent_pool._is_valid,
+                                        lambda x: \
+                                        _mark_weakpool_invalid(weakself));
+
+        # mark pool as valid
+        self._is_valid = lambda: 1
+
+    def _mark_invalid(self):
+        """Mark pool as invalid"""
+        if self.valid():
+            # Mark invalid
+            del self._is_valid
+
+            # Free up memory
+            del self._parent_pool
+            if hasattr(self, "_weakref"):
+                del self._weakref
+
+
+# Initialize application-level pool
+if has_subversion:
+    Pool()
+
+
+class SubversionConnector(Component):
+
+    implements(IRepositoryConnector)
+
+    def get_supported_types(self):
+        global has_subversion
+        if has_subversion:
+            yield ("svnfs", 4)
+            yield ("svn", 2)
+
+    def get_repository(self, type, dir, authname):
+        """Return a `SubversionRepository`.
+
+        The repository is generally wrapped in a `CachedRepository`,
+        unless `direct-svn-fs` is the specified type.
+        """
+        authz = None
+        if authname:
+            authz = SubversionAuthorizer(self.env, authname)
+        repos = SubversionRepository(dir, authz, self.log)
+        return CachedRepository(self.env.get_db_cnx(), repos, authz, self.log)
+
+
+class SubversionRepository(Repository):
+    """
+    Repository implementation based on the svn.fs API.
+    """
+
+    def __init__(self, path, authz, log):
+        self.path = path # might be needed by __del__()/close()
+        self.log = log
+        if core.SVN_VER_MAJOR < 1:
+            raise TracError("Subversion >= 1.0 required: Found %d.%d.%d" % \
+                            (core.SVN_VER_MAJOR,
+                             core.SVN_VER_MINOR,
+                             core.SVN_VER_MICRO))
+        self.pool = Pool()
+        
+        # Remove any trailing slash or else subversion might abort
+        if isinstance(path, unicode):
+            path = path.encode('utf-8')
+        path = os.path.normpath(path).replace('\\', '/')
+        self.path = repos.svn_repos_find_root_path(path, self.pool())
+        if self.path is None:
+            raise TracError("%s does not appear to be a Subversion repository." \
+                            % path)
+
+        self.repos = repos.svn_repos_open(self.path, self.pool())
+        self.fs_ptr = repos.svn_repos_fs(self.repos)
+        
+        uuid = fs.get_uuid(self.fs_ptr, self.pool())
+        name = 'svn:%s:%s' % (uuid, path)
+
+        Repository.__init__(self, name, authz, log)
+
+        if self.path != path:
+            self.scope = path[len(self.path):]
+            if not self.scope[-1] == '/':
+                self.scope += '/'
+        else:
+            self.scope = '/'
+        assert self.scope[0] == '/'
+        
+        self.log.debug("Opening subversion file-system at %s with scope %s" \
+                       % (self.path, self.scope))
+        self.youngest = None
+        self.oldest = None
+
+    def __del__(self):
+        self.close()
+
+    def has_node(self, path, rev, pool=None):
+        if not pool:
+            pool = self.pool
+        rev_root = fs.revision_root(self.fs_ptr, rev, pool())
+        node_type = fs.check_path(rev_root, _to_svn(self.scope, path), pool())
+        return node_type in _kindmap
+
+    def normalize_path(self, path):
+        return _normalize_path(path)
+
+    def normalize_rev(self, rev):
+        try:
+            rev =  int(rev)
+        except (ValueError, TypeError):
+            rev = None
+        if rev is None:
+            rev = self.youngest_rev
+        elif rev > self.youngest_rev:
+            raise NoSuchChangeset(rev)
+        return rev
+
+    def close(self):
+        self.log.debug("Closing subversion file-system at %s" % self.path)
+        self.repos = None
+        self.fs_ptr = None
+        self.pool = None
+
+    def get_changeset(self, rev):
+        return SubversionChangeset(int(rev), self.authz, self.scope,
+                                   self.fs_ptr, self.pool)
+
+    def get_node(self, path, rev=None):
+        path = path or ''
+        self.authz.assert_permission(posixpath.join(self.scope, path))
+        if path and path[-1] == '/':
+            path = path[:-1]
+
+        rev = self.normalize_rev(rev)
+
+        return SubversionNode(path, rev, self.authz, self.scope, self.fs_ptr,
+                              self.pool)
+
+    def _history(self, path, start, end, limit=None, pool=None):
+        return _get_history(_to_svn(self.scope, path), self.authz, self.fs_ptr,
+                            pool or self.pool, start, end, limit)
+
+    def _previous_rev(self, rev, path='', pool=None):
+        if rev > 1: # don't use oldest here, as it's too expensive
+            try:
+                for _, prev in self._history(path, 0, rev-1, limit=1,
+                                             pool=pool):
+                    return prev
+            except (SystemError, # "null arg to internal routine" in 1.2.x
+                    core.SubversionException): # in 1.3.x
+                pass
+        return None
+    
+
+    def get_oldest_rev(self):
+        if self.oldest is None:
+            self.oldest = 1
+            if self.scope != '/':
+                self.oldest = self.next_rev(0, find_initial_rev=True)
+        return self.oldest
+
+    def get_youngest_rev(self):
+        if not self.youngest:
+            self.youngest = fs.youngest_rev(self.fs_ptr, self.pool())
+            if self.scope != '/':
+                for path, rev in self._history('', 0, self.youngest, limit=1):
+                    self.youngest = rev
+        return self.youngest
+
+    def previous_rev(self, rev, path=''):
+        rev = self.normalize_rev(rev)
+        return self._previous_rev(rev, path)
+
+    def next_rev(self, rev, path='', find_initial_rev=False):
+        rev = self.normalize_rev(rev)
+        next = rev + 1
+        youngest = self.youngest_rev
+        subpool = Pool(self.pool)
+        while next <= youngest:
+            subpool.clear()            
+            try:
+                for _, next in self._history(path, rev+1, next, limit=1,
+                                             pool=subpool):
+                    return next
+            except (SystemError, # "null arg to internal routine" in 1.2.x
+                    core.SubversionException): # in 1.3.x
+                if not find_initial_rev:
+                    return next # a 'delete' event is also interesting...
+            next += 1
+        return None
+
+    def rev_older_than(self, rev1, rev2):
+        return self.normalize_rev(rev1) < self.normalize_rev(rev2)
+
+    def get_youngest_rev_in_cache(self, db):
+        """Get the latest stored revision by sorting the revision strings
+        numerically
+        """
+        cursor = db.cursor()
+        cursor.execute("SELECT rev FROM revision "
+                       "ORDER BY -LENGTH(rev), rev DESC LIMIT 1")
+        row = cursor.fetchone()
+        return row and row[0] or None
+
+    def get_path_history(self, path, rev=None, limit=None):
+        path = self.normalize_path(path)
+        rev = self.normalize_rev(rev)
+        expect_deletion = False
+        subpool = Pool(self.pool)
+        while rev:
+            subpool.clear()
+            if self.has_node(path, rev, subpool):
+                if expect_deletion:
+                    # it was missing, now it's there again:
+                    #  rev+1 must be a delete
+                    yield path, rev+1, Changeset.DELETE
+                newer = None # 'newer' is the previously seen history tuple
+                older = None # 'older' is the currently examined history tuple
+                for p, r in _get_history(_to_svn(self.scope, path), self.authz,
+                                         self.fs_ptr, subpool, 0, rev, limit):
+                    older = (_path_within_scope(self.scope, p), r,
+                             Changeset.ADD)
+                    rev = self._previous_rev(r, pool=subpool)
+                    if newer:
+                        if older[0] == path:
+                            # still on the path: 'newer' was an edit
+                            yield newer[0], newer[1], Changeset.EDIT
+                        else:
+                            # the path changed: 'newer' was a copy
+                            rev = self._previous_rev(newer[1], pool=subpool)
+                            # restart before the copy op
+                            yield newer[0], newer[1], Changeset.COPY
+                            older = (older[0], older[1], 'unknown')
+                            break
+                    newer = older
+                if older:
+                    # either a real ADD or the source of a COPY
+                    yield older
+            else:
+                expect_deletion = True
+                rev = self._previous_rev(rev, pool=subpool)
+
+    def get_changes(self, old_path, old_rev, new_path, new_rev,
+                   ignore_ancestry=0):
+        old_node = new_node = None
+        old_rev = self.normalize_rev(old_rev)
+        new_rev = self.normalize_rev(new_rev)
+        if self.has_node(old_path, old_rev):
+            old_node = self.get_node(old_path, old_rev)
+        else:
+            raise NoSuchNode(old_path, old_rev, 'The Base for Diff is invalid')
+        if self.has_node(new_path, new_rev):
+            new_node = self.get_node(new_path, new_rev)
+        else:
+            raise NoSuchNode(new_path, new_rev, 'The Target for Diff is invalid')
+        if new_node.kind != old_node.kind:
+            raise TracError('Diff mismatch: Base is a %s (%s in revision %s) '
+                            'and Target is a %s (%s in revision %s).' \
+                            % (old_node.kind, old_path, old_rev,
+                               new_node.kind, new_path, new_rev))
+        subpool = Pool(self.pool)
+        if new_node.isdir:
+            editor = DiffChangeEditor()
+            e_ptr, e_baton = delta.make_editor(editor, subpool())
+            old_root = fs.revision_root(self.fs_ptr, old_rev, subpool())
+            new_root = fs.revision_root(self.fs_ptr, new_rev, subpool())
+            def authz_cb(root, path, pool): return 1
+            text_deltas = 0 # as this is anyway re-done in Diff.py...
+            entry_props = 0 # "... typically used only for working copy updates"
+            repos.svn_repos_dir_delta(old_root,
+                                      _to_svn(self.scope, old_path), '',
+                                      new_root,
+                                      _to_svn(self.scope + new_path),
+                                      e_ptr, e_baton, authz_cb,
+                                      text_deltas,
+                                      1, # directory
+                                      entry_props,
+                                      ignore_ancestry,
+                                      subpool())
+            for path, kind, change in editor.deltas:
+                path = _from_svn(path)
+                old_node = new_node = None
+                if change != Changeset.ADD:
+                    old_node = self.get_node(posixpath.join(old_path, path),
+                                             old_rev)
+                if change != Changeset.DELETE:
+                    new_node = self.get_node(posixpath.join(new_path, path),
+                                             new_rev)
+                else:
+                    kind = _kindmap[fs.check_path(old_root,
+                                                  _to_svn(self.scope,
+                                                          old_node.path),
+                                                  subpool())]
+                yield  (old_node, new_node, kind, change)
+        else:
+            old_root = fs.revision_root(self.fs_ptr, old_rev, subpool())
+            new_root = fs.revision_root(self.fs_ptr, new_rev, subpool())
+            if fs.contents_changed(old_root, _to_svn(self.scope, old_path),
+                                   new_root, _to_svn(self.scope, new_path),
+                                   subpool()):
+                yield (old_node, new_node, Node.FILE, Changeset.EDIT)
+
+
+class SubversionNode(Node):
+
+    def __init__(self, path, rev, authz, scope, fs_ptr, pool=None):
+        self.authz = authz
+        self.scope = scope
+        self._scoped_svn_path = _to_svn(scope, path)
+        self.fs_ptr = fs_ptr
+        self.pool = Pool(pool)
+        self._requested_rev = rev
+
+        self.root = fs.revision_root(fs_ptr, rev, self.pool())
+        node_type = fs.check_path(self.root, self._scoped_svn_path,
+                                  self.pool())
+        if not node_type in _kindmap:
+            raise NoSuchNode(path, rev)
+        cr = fs.node_created_rev(self.root, self._scoped_svn_path, self.pool())
+        cp = fs.node_created_path(self.root, self._scoped_svn_path, self.pool())
+        # Note: `cp` differs from `path` if the last change was a copy,
+        #        In that case, `path` doesn't even exist at `cr`.
+        #        The only guarantees are:
+        #          * this node exists at (path,rev)
+        #          * the node existed at (created_path,created_rev)
+        # Also, `cp` might well be out of the scope of the repository,
+        # in this case, we _don't_ use the ''create'' information.
+        if _is_path_within_scope(self.scope, cp):
+            self.created_rev = cr
+            self.created_path = _path_within_scope(self.scope, _from_svn(cp))
+        else:
+            self.created_rev, self.created_path = rev, path
+        self.rev = self.created_rev
+        # TODO: check node id
+        Node.__init__(self, path, self.rev, _kindmap[node_type])
+
+    def get_content(self):
+        if self.isdir:
+            return None
+        s = core.Stream(fs.file_contents(self.root, self._scoped_svn_path,
+                                         self.pool()))
+        # Make sure the stream object references the pool to make sure the pool
+        # is not destroyed before the stream object.
+        s._pool = self.pool
+        return s
+
+    def get_entries(self):
+        if self.isfile:
+            return
+        pool = Pool(self.pool)
+        entries = fs.dir_entries(self.root, self._scoped_svn_path, pool())
+        for item in entries.keys():
+            path = posixpath.join(self.path, _from_svn(item))
+            if not self.authz.has_permission(path):
+                continue
+            yield SubversionNode(path, self._requested_rev, self.authz,
+                                 self.scope, self.fs_ptr, self.pool)
+
+    def get_history(self,limit=None):
+        newer = None # 'newer' is the previously seen history tuple
+        older = None # 'older' is the currently examined history tuple
+        pool = Pool(self.pool)
+        for path, rev in _get_history(self._scoped_svn_path, self.authz,
+                                      self.fs_ptr, pool,
+                                      0, self._requested_rev, limit):
+            path = _path_within_scope(self.scope, path)
+            if rev > 0 and path:
+                older = (path, rev, Changeset.ADD)
+                if newer:
+                    change = newer[0] == older[0] and Changeset.EDIT or \
+                             Changeset.COPY
+                    newer = (newer[0], newer[1], change)
+                    yield newer
+                newer = older
+        if newer:
+            yield newer
+
+#    def get_previous(self):
+#        # FIXME: redo it with fs.node_history
+
+    def get_properties(self):
+        props = fs.node_proplist(self.root, self._scoped_svn_path, self.pool())
+        for name, value in props.items():
+            # Note that property values can be arbitrary binary values
+            # so we can't assume they are UTF-8 strings...
+            props[_from_svn(name)] = to_unicode(value)
+        return props
+
+    def get_content_length(self):
+        if self.isdir:
+            return None
+        return fs.file_length(self.root, self._scoped_svn_path, self.pool())
+
+    def get_content_type(self):
+        if self.isdir:
+            return None
+        return self._get_prop(core.SVN_PROP_MIME_TYPE)
+
+    def get_last_modified(self):
+        date = fs.revision_prop(self.fs_ptr, self.created_rev,
+                                core.SVN_PROP_REVISION_DATE, self.pool())
+        return core.svn_time_from_cstring(date, self.pool()) / 1000000
+
+    def _get_prop(self, name):
+        return fs.node_prop(self.root, self._scoped_svn_path, name, self.pool())
+
+
+class SubversionChangeset(Changeset):
+
+    def __init__(self, rev, authz, scope, fs_ptr, pool=None):
+        self.rev = rev
+        self.authz = authz
+        self.scope = scope
+        self.fs_ptr = fs_ptr
+        self.pool = Pool(pool)
+        message = self._get_prop(core.SVN_PROP_REVISION_LOG)
+        author = self._get_prop(core.SVN_PROP_REVISION_AUTHOR)
+        date = self._get_prop(core.SVN_PROP_REVISION_DATE)
+        date = core.svn_time_from_cstring(date, self.pool()) / 1000000
+        Changeset.__init__(self, rev, message, author, date)
+
+    def get_changes(self):
+        pool = Pool(self.pool)
+        tmp = Pool(pool)
+        root = fs.revision_root(self.fs_ptr, self.rev, pool())
+        editor = repos.RevisionChangeCollector(self.fs_ptr, self.rev, pool())
+        e_ptr, e_baton = delta.make_editor(editor, pool())
+        repos.svn_repos_replay(root, e_ptr, e_baton, pool())
+
+        idx = 0
+        copies, deletions = {}, {}
+        changes = []
+        revroots = {}
+        for path, change in editor.changes.items():
+            #assert path == change.path or change.base_path
+            
+            # Filtering on `path`
+            if not (_is_path_within_scope(self.scope, path) and \
+                    self.authz.has_permission(path)):
+                continue
+
+            path = change.path
+            base_path = change.base_path
+            base_rev = change.base_rev
+
+            # Ensure `base_path` is within the scope
+            if not (_is_path_within_scope(self.scope, base_path) and \
+                    self.authz.has_permission(base_path)):
+                base_path, base_rev = None, -1
+
+            # Determine the action
+            if not path:                # deletion
+                if base_path:
+                    action = Changeset.DELETE
+                    deletions[base_path] = idx
+                elif self.scope:        # root property change
+                    action = Changeset.EDIT
+                else:                   # deletion outside of scope, ignore
+                    continue
+            elif change.added or not base_path: # add or copy
+                action = Changeset.ADD
+                if base_path and base_rev:
+                    action = Changeset.COPY
+                    copies[base_path] = idx
+            else:
+                action = Changeset.EDIT
+                # identify the most interesting base_path/base_rev
+                # in terms of last changed information (see r2562)
+                if revroots.has_key(base_rev):
+                    b_root = revroots[base_rev]
+                else:
+                    b_root = fs.revision_root(self.fs_ptr, base_rev, pool())
+                    revroots[base_rev] = b_root
+                tmp.clear()
+                cbase_path = fs.node_created_path(b_root, base_path, tmp())
+                cbase_rev = fs.node_created_rev(b_root, base_path, tmp()) 
+                # give up if the created path is outside the scope
+                if _is_path_within_scope(self.scope, cbase_path):
+                    base_path, base_rev = cbase_path, cbase_rev
+
+            kind = _kindmap[change.item_kind]
+            path = _path_within_scope(self.scope, _from_svn(path or base_path))
+            base_path = _path_within_scope(self.scope, _from_svn(base_path))
+            changes.append([path, kind, action, base_path, base_rev])
+            idx += 1
+
+        moves = []
+        for k,v in copies.items():
+            if k in deletions:
+                changes[v][2] = Changeset.MOVE
+                moves.append(deletions[k])
+        offset = 0
+        moves.sort()
+        for i in moves:
+            del changes[i - offset]
+            offset += 1
+
+        changes.sort()
+        for change in changes:
+            yield tuple(change)
+
+    def _get_prop(self, name):
+        return fs.revision_prop(self.fs_ptr, self.rev, name, self.pool())
+
+
+#
+# Delta editor for diffs between arbitrary nodes
+#
+# Note 1: the 'copyfrom_path' and 'copyfrom_rev' information is not used
+#         because 'repos.svn_repos_dir_delta' *doesn't* provide it.
+#
+# Note 2: the 'dir_baton' is the path of the parent directory
+#
+
+class DiffChangeEditor(delta.Editor): 
+
+    def __init__(self):
+        self.deltas = []
+    
+    # -- svn.delta.Editor callbacks
+
+    def open_root(self, base_revision, dir_pool):
+        return ('/', Changeset.EDIT)
+
+    def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev,
+                      dir_pool):
+        self.deltas.append((path, Node.DIRECTORY, Changeset.ADD))
+        return (path, Changeset.ADD)
+
+    def open_directory(self, path, dir_baton, base_revision, dir_pool):
+        return (path, dir_baton[1])
+
+    def change_dir_prop(self, dir_baton, name, value, pool):
+        path, change = dir_baton
+        if change != Changeset.ADD:
+            self.deltas.append((path, Node.DIRECTORY, change))
+
+    def delete_entry(self, path, revision, dir_baton, pool):
+        self.deltas.append((path, None, Changeset.DELETE))
+
+    def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision,
+                 dir_pool):
+        self.deltas.append((path, Node.FILE, Changeset.ADD))
+
+    def open_file(self, path, dir_baton, dummy_rev, file_pool):
+        self.deltas.append((path, Node.FILE, Changeset.EDIT))
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/tests/__init__.py
@@ -0,0 +1,15 @@
+import unittest
+
+from trac.versioncontrol.tests import cache, diff, svn_authz, svn_fs
+
+def suite():
+
+    suite = unittest.TestSuite()
+    suite.addTest(cache.suite())
+    suite.addTest(diff.suite())
+    suite.addTest(svn_authz.suite())
+    suite.addTest(svn_fs.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/tests/cache.py
@@ -0,0 +1,145 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+from trac.log import logger_factory
+from trac.test import Mock, InMemoryDatabase
+from trac.versioncontrol import Repository, Changeset, Node
+from trac.versioncontrol.cache import CachedRepository
+
+import time
+import unittest
+
+
+class CacheTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.db = InMemoryDatabase()
+        self.log = logger_factory('test')
+
+    def test_initial_sync_with_empty_repos(self):
+        changeset = Mock(Changeset, 0, '', '', 42000,
+                         get_changes=lambda: [])
+        repos = Mock(Repository, 'test-repos', None, self.log,
+                     get_changeset=lambda x: changeset,
+                     get_oldest_rev=lambda: 0,
+                     get_youngest_rev=lambda: 0,
+                     normalize_rev=lambda x: x,
+                     next_rev=lambda x: None)
+        cache = CachedRepository(self.db, repos, None, self.log)
+        cache.sync()
+
+        cursor = self.db.cursor()
+        cursor.execute("SELECT rev,time,author,message FROM revision")
+        self.assertEquals(('0', 42000, '', ''), cursor.fetchone())
+        cursor.execute("SELECT COUNT(*) FROM node_change")
+        self.assertEquals(0, cursor.fetchone()[0])
+
+    def test_initial_sync(self):
+        changes = [('trunk', Node.DIRECTORY, Changeset.ADD, None, None),
+                   ('trunk/README', Node.FILE, Changeset.ADD, None, None)]
+        changesets = [Mock(Changeset, 0, '', '', 41000,
+                           get_changes=lambda: []),
+                      Mock(Changeset, 1, 'Import', 'joe', 42000,
+                           get_changes=lambda: iter(changes))]
+        repos = Mock(Repository, 'test-repos', None, self.log,
+                     get_changeset=lambda x: changesets[int(x)],
+                     get_oldest_rev=lambda: 0,
+                     get_youngest_rev=lambda: 1,
+                     normalize_rev=lambda x: x,
+                     next_rev=lambda x: int(x) == 0 and 1 or None)
+        cache = CachedRepository(self.db, repos, None, self.log)
+        cache.sync()
+
+        cursor = self.db.cursor()
+        cursor.execute("SELECT rev,time,author,message FROM revision")
+        self.assertEquals(('0', 41000, '', ''), cursor.fetchone())
+        self.assertEquals(('1', 42000, 'joe', 'Import'), cursor.fetchone())
+        self.assertEquals(None, cursor.fetchone())
+        cursor.execute("SELECT rev,path,node_type,change_type,base_path,"
+                       "base_rev FROM node_change")
+        self.assertEquals(('1', 'trunk', 'D', 'A', None, None),
+                          cursor.fetchone())
+        self.assertEquals(('1', 'trunk/README', 'F', 'A', None, None),
+                          cursor.fetchone())
+        self.assertEquals(None, cursor.fetchone())
+
+    def test_update_sync(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO revision (rev,time,author,message) "
+                       "VALUES (0,41000,'','')")
+        cursor.execute("INSERT INTO revision (rev,time,author,message) "
+                       "VALUES (1,42000,'joe','Import')")
+        cursor.executemany("INSERT INTO node_change (rev,path,node_type,"
+                           "change_type,base_path,base_rev) "
+                           "VALUES ('1',%s,%s,%s,%s,%s)",
+                           [('trunk', 'D', 'A', None, None),
+                            ('trunk/README', 'F', 'A', None, None)])
+
+        changes = [('trunk/README', Node.FILE, Changeset.EDIT, 'trunk/README', 1)]
+        changeset = Mock(Changeset, 2, 'Update', 'joe', 42042,
+                         get_changes=lambda: iter(changes))
+        repos = Mock(Repository, 'test-repos', None, self.log,
+                     get_changeset=lambda x: changeset,
+                     get_youngest_rev=lambda: 2,
+                     next_rev=lambda x: int(x) == 1 and 2 or None)
+        cache = CachedRepository(self.db, repos, None, self.log)
+        cache.sync()
+
+        cursor = self.db.cursor()
+        cursor.execute("SELECT time,author,message FROM revision WHERE rev='2'")
+        self.assertEquals((42042, 'joe', 'Update'), cursor.fetchone())
+        self.assertEquals(None, cursor.fetchone())
+        cursor.execute("SELECT path,node_type,change_type,base_path,base_rev "
+                       "FROM node_change WHERE rev='2'")
+        self.assertEquals(('trunk/README', 'F', 'E', 'trunk/README', '1'),
+                          cursor.fetchone())
+        self.assertEquals(None, cursor.fetchone())
+
+    def test_get_changes(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO revision (rev,time,author,message) "
+                       "VALUES (0,41000,'','')")
+        cursor.execute("INSERT INTO revision (rev,time,author,message) "
+                       "VALUES (1,42000,'joe','Import')")
+        cursor.executemany("INSERT INTO node_change (rev,path,node_type,"
+                           "change_type,base_path,base_rev) "
+                           "VALUES ('1',%s,%s,%s,%s,%s)",
+                           [('trunk', 'D', 'A', None, None),
+                            ('trunk/README', 'F', 'A', None, None)])
+
+        repos = Mock(Repository, 'test-repos', None, self.log,
+                     get_changeset=lambda x: None,
+                     get_youngest_rev=lambda: 1,
+                     next_rev=lambda x: None, normalize_rev=lambda rev: rev)
+        cache = CachedRepository(self.db, repos, None, self.log)
+        self.assertEqual(1, cache.youngest_rev)
+        changeset = cache.get_changeset(1)
+        self.assertEqual('joe', changeset.author)
+        self.assertEqual('Import', changeset.message)
+        self.assertEqual(42000, changeset.date)
+        changes = changeset.get_changes()
+        self.assertEqual(('trunk', Node.DIRECTORY, Changeset.ADD, None, None),
+                         changes.next())
+        self.assertEqual(('trunk/README', Node.FILE, Changeset.ADD, None, None),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+
+def suite():
+    return unittest.makeSuite(CacheTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/tests/diff.py
@@ -0,0 +1,104 @@
+from trac.versioncontrol import diff
+
+import unittest
+
+
+class DiffTestCase(unittest.TestCase):
+
+    def test_get_change_extent(self):
+        self.assertEqual((3, 0), diff._get_change_extent('xxx', 'xxx'))
+        self.assertEqual((0, 0), diff._get_change_extent('', 'xxx'))
+        self.assertEqual((0, 0), diff._get_change_extent('xxx', ''))
+        self.assertEqual((0, 0), diff._get_change_extent('xxx', 'yyy'))
+        self.assertEqual((1, -1), diff._get_change_extent('xxx', 'xyx'))
+        self.assertEqual((1, -1), diff._get_change_extent('xxx', 'xyyyx'))
+        self.assertEqual((1, 0), diff._get_change_extent('xy', 'xzz'))
+        self.assertEqual((1, -1), diff._get_change_extent('xyx', 'xzzx'))
+        self.assertEqual((1, -1), diff._get_change_extent('xzzx', 'xyx'))
+
+    def test_insert_blank_line(self):
+        opcodes = diff._get_opcodes(['A', 'B'], ['A', 'B', ''],
+                                     ignore_blank_lines=0)
+        self.assertEqual(('equal', 0, 2, 0, 2), opcodes.next())
+        self.assertEqual(('insert', 2, 2, 2, 3), opcodes.next())
+
+        opcodes = diff._get_opcodes(['A', 'B'], ['A', 'B', ''],
+                                     ignore_blank_lines=1)
+        self.assertEqual(('equal', 0, 2, 0, 3), opcodes.next())
+
+        opcodes = diff._get_opcodes(['A'], ['A', 'B', ''],
+                                     ignore_blank_lines=1)
+        self.assertEqual(('equal', 0, 1, 0, 1), opcodes.next())
+        self.assertEqual(('insert', 1, 1, 1, 3), opcodes.next())
+
+    def test_delete_blank_line(self):
+        opcodes = diff._get_opcodes(['A', 'B', ''], ['A', 'B'],
+                                     ignore_blank_lines=0)
+        self.assertEqual(('equal', 0, 2, 0, 2), opcodes.next())
+        self.assertEqual(('delete', 2, 3, 2, 2), opcodes.next())
+
+        opcodes = diff._get_opcodes(['A', 'B', ''], ['A', 'B'],
+                                     ignore_blank_lines=1)
+        self.assertEqual(('equal', 0, 3, 0, 2), opcodes.next())
+
+        opcodes = diff._get_opcodes(['A', 'B', ''], ['A'],
+                                     ignore_blank_lines=1)
+        self.assertEqual(('equal', 0, 1, 0, 1), opcodes.next())
+        self.assertEqual(('delete', 1, 3, 1, 1), opcodes.next())
+
+    def test_space_changes(self):
+        opcodes = diff._get_opcodes(['A', 'B b'], ['A', 'B  b'],
+                                     ignore_space_changes=0)
+        self.assertEqual(('equal', 0, 1, 0, 1), opcodes.next())
+        self.assertEqual(('replace', 1, 2, 1, 2), opcodes.next())
+
+        opcodes = diff._get_opcodes(['A', 'B b'], ['A', 'B  b'],
+                                     ignore_space_changes=1)
+        self.assertEqual(('equal', 0, 2, 0, 2), opcodes.next())
+
+    def test_case_changes(self):
+        opcodes = diff._get_opcodes(['A', 'B b'], ['A', 'B B'],
+                                     ignore_case=0)
+        self.assertEqual(('equal', 0, 1, 0, 1), opcodes.next())
+        self.assertEqual(('replace', 1, 2, 1, 2), opcodes.next())
+
+        opcodes = diff._get_opcodes(['A', 'B b'], ['A', 'B B'],
+                                     ignore_case=1)
+        self.assertEqual(('equal', 0, 2, 0, 2), opcodes.next())
+
+    def test_space_and_case_changes(self):
+        opcodes = diff._get_opcodes(['A', 'B b'], ['A', 'B  B'],
+                                     ignore_case=0, ignore_space_changes=0)
+        self.assertEqual(('equal', 0, 1, 0, 1), opcodes.next())
+        self.assertEqual(('replace', 1, 2, 1, 2), opcodes.next())
+
+        opcodes = diff._get_opcodes(['A', 'B b'], ['A', 'B  B'],
+                                     ignore_case=1, ignore_space_changes=1)
+        self.assertEqual(('equal', 0, 2, 0, 2), opcodes.next())
+
+    def test_grouped_opcodes_context1(self):
+        opcodes = diff._get_opcodes(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'],
+                                    ['A', 'B', 'C', 'd', 'e', 'f', 'G', 'H'])
+        groups = diff._group_opcodes(opcodes, n=1)
+        group = groups.next()
+        self.assertEqual(('equal', 2, 3, 2, 3), group[0])
+        self.assertEqual(('replace', 3, 6, 3, 6), group[1])
+        self.assertEqual(('equal', 6, 7, 6, 7), group[2])
+
+    def test_grouped_opcodes_insert_blank_line_at_top(self):
+        """
+        Regression test for #2090. Make sure that the equal block following an
+        insert at the top of a file is correct.
+        """
+        opcodes = diff._get_opcodes(['B', 'C', 'D', 'E', 'F', 'G'],
+                                    ['A', 'B', 'C', 'D', 'E', 'F', 'G'])
+        groups = diff._group_opcodes(opcodes, n=3)
+        self.assertEqual([('insert', 0, 0, 0, 1), ('equal', 0, 3, 1, 4)],
+                         groups.next())
+    
+
+def suite():
+    return unittest.makeSuite(DiffTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/tests/svn_authz.py
@@ -0,0 +1,224 @@
+from trac.versioncontrol import svn_authz
+
+import unittest
+import sys
+
+def tests():
+  """
+  Subversion Authz File Permissions
+  =================================
+  
+  Setup code
+  ----------
+  We'll use the ``make_auth`` method to create Authorizer objects
+  for testing the use of authz files.  ``make_auth`` takes a module name
+  and a string for the authz configuration contents.
+  
+  >>> from trac.versioncontrol.svn_authz import RealSubversionAuthorizer
+  >>> from StringIO import StringIO
+  >>> make_auth = lambda mod, cfg: RealSubversionAuthorizer(None,
+  ...                   'user', mod, None, StringIO(cfg))
+  
+  
+  Simple operation
+  ----------------
+  Returns 1 if no path is given:
+      >>> int(make_auth('', '').has_permission(None))
+      1
+  
+  By default read permission is not enabled:
+      >>> int(make_auth('', '').has_permission('/'))
+      0
+  
+  Read and Write Permissions
+  ----------------------
+  Trac is only concerned about read permissions.
+      >>> a = make_auth('', '''
+      ... [/readonly]
+      ... user = r
+      ... [/writeonly]
+      ... user = w
+      ... [/readwrite]
+      ... user = rw
+      ... [/empty]
+      ... user = 
+      ... ''')
+  
+  Permissions of 'r' or 'rw' will allow access:
+      >>> int(a.has_permission('/readonly'))
+      1
+      >>> int(a.has_permission('/readwrite'))
+      1
+  
+  If only 'w' permission is given, Trac does not allow access:
+      >>> int(a.has_permission('/writeonly'))
+      0
+  
+  And an empty permission does not give access:
+      >>> int(a.has_permission('/empty'))
+      0
+  
+  Trailing Slashes
+  ----------------
+  Checks all combinations of trailing slashes in the configuration
+  or in the path parameter:
+      >>> a = make_auth('', '''
+      ... [/a]
+      ... user = r
+      ... [/b/]
+      ... user = r
+      ... ''')
+      >>> int(a.has_permission('/a'))
+      1
+      >>> int(a.has_permission('/a/'))
+      1
+      >>> int(a.has_permission('/b'))
+      1
+      >>> int(a.has_permission('/b/'))
+      1
+  
+  
+  Module Usage
+  ------------
+  If a module name is specified, the rules used are specific to the module.
+      >>> a = make_auth('module', '''
+      ... [module:/a]
+      ... user = r
+      ... [other:/b]
+      ... user = r
+      ... ''')
+      >>> int(a.has_permission('/a'))
+      1
+      >>> int(a.has_permission('/b'))
+      0
+  
+  If a module is specified, but the configuration contains a non-module
+  path, the non-module path can still apply:
+      >>> int(make_auth('module', '''
+      ... [/a]
+      ... user = r
+      ... ''').has_permission('/a'))
+      1
+  
+  However, the module-specific rule will take precedence if both exist:
+      >>> int(make_auth('module', '''
+      ... [module:/a]
+      ... user = 
+      ... [/a]
+      ... user = r
+      ... ''').has_permission('/a'))
+      0
+  
+  
+  Groups and Wildcards
+  --------------------
+  Authz provides a * wildcard for matching any user:
+      >>> int(make_auth('', '''
+      ... [/a]
+      ... * = r
+      ... ''').has_permission('/a'))
+      1
+  
+  Groups are specified in a separate section and used with an @ prefix:
+      >>> int(make_auth('', '''
+      ... [groups]
+      ... grp = user
+      ... [/a]
+      ... @grp = r
+      ... ''').has_permission('/a'))
+      1
+
+  Groups can also be members of other groups:
+      >>> int(make_auth('', '''
+      ... [groups]
+      ... grp1 = user
+      ... grp2 = @grp1
+      ... [/a]
+      ... @grp2 = r
+      ... ''').has_permission('/a'))
+      1
+
+  Groups should not be defined cyclically, but they are handled appropriately
+  to avoid infinite loops:
+      >>> int(make_auth('', '''
+      ... [groups]
+      ... grp1 = @grp2
+      ... grp2 = @grp3
+      ... grp3 = @grp1, user
+      ... [/a]
+      ... @grp1 = r
+      ... ''').has_permission('/a'))
+      1
+  
+  If more than one group matches at the specific path, access is granted
+  if any of the group rules allow access.
+      >>> a = make_auth('', '''
+      ... [groups]
+      ... grp1 = user
+      ... grp2 = user
+      ... [/a]
+      ... @grp1 = r
+      ... @grp2 = 
+      ... [/b]
+      ... @grp1 = 
+      ... @grp2 = r
+      ... ''')
+      >>> int(a.has_permission('/a'))
+      1
+      >>> int(a.has_permission('/b'))
+      1
+  
+  
+  Precedence
+  ----------
+  Precedence is user, group, then *:
+      >>> a = make_auth('', '''
+      ... [groups]
+      ... grp = user
+      ... [/a]
+      ... @grp = r
+      ... user = 
+      ... [/b]
+      ... * = r
+      ... @grp = 
+      ... ''')
+  
+  User specific permission overrides the group permission:
+      >>> int(a.has_permission('/a'))
+      0
+  
+  And group permission overrides the * permission:
+      >>> int(a.has_permission('/b'))
+      0
+  
+  The most specific matching path takes precedence:
+      >>> a = make_auth('', '''
+      ... [/]
+      ... * = r
+      ... [/b]
+      ... user = 
+      ... ''')
+      >>> int(a.has_permission('/'))
+      1
+      >>> int(a.has_permission('/a'))
+      1
+      >>> int(a.has_permission('/b'))
+      0
+  
+  Changeset Permissions
+  ---------------------
+  A test should go here for the changeset permissions.
+  """
+
+def suite():
+    try:
+        from doctest import DocTestSuite
+        return DocTestSuite(sys.modules[__name__])
+    except ImportError:
+        print>>sys.stderr, "WARNING: DocTestSuite required to run these tests"
+    return unittest.TestSuite()
+
+if __name__ == '__main__':
+    runner = unittest.TextTestRunner()
+    runner.run(suite())
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/tests/svn_fs.py
@@ -0,0 +1,713 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+import os.path
+import stat
+import shutil
+import sys
+import tempfile
+import unittest
+
+from StringIO import StringIO
+
+try:
+    from svn import core, repos
+    has_svn = True
+except:
+    has_svn = False
+
+from trac.log import logger_factory
+from trac.test import TestSetup
+from trac.core import TracError
+from trac.versioncontrol import Changeset, Node
+from trac.versioncontrol.svn_fs import SubversionRepository
+
+REPOS_PATH = os.path.join(tempfile.gettempdir(), 'trac-svnrepos')
+
+
+class SubversionRepositoryTestSetup(TestSetup):
+
+    def setUp(self):
+        dumpfile = open(os.path.join(os.path.split(__file__)[0],
+                                     'svnrepos.dump'))
+
+        core.apr_initialize()
+        pool = core.svn_pool_create(None)
+        dumpstream = None
+        try:
+            r = repos.svn_repos_create(REPOS_PATH, '', '', None, None, pool)
+            if hasattr(repos, 'svn_repos_load_fs2'):
+                repos.svn_repos_load_fs2(r, dumpfile, StringIO(),
+                                        repos.svn_repos_load_uuid_default, '',
+                                        0, 0, None, pool)
+            else:
+                dumpstream = core.svn_stream_from_aprfile(dumpfile, pool)
+                repos.svn_repos_load_fs(r, dumpstream, None,
+                                        repos.svn_repos_load_uuid_default, '',
+                                        None, None, pool)
+        finally:
+            if dumpstream:
+                core.svn_stream_close(dumpstream)
+            core.svn_pool_destroy(pool)
+            core.apr_terminate()
+
+    def tearDown(self):
+        if os.name == 'nt':
+            # The Windows version of 'shutil.rmtree' doesn't override the
+            # permissions of read-only files, so we have to do it ourselves:
+            format_file = os.path.join(REPOS_PATH, 'db', 'format')
+            if os.path.isfile(format_file):
+                os.chmod(format_file, stat.S_IRWXU)
+            os.chmod(os.path.join(REPOS_PATH, 'format'), stat.S_IRWXU)
+        shutil.rmtree(REPOS_PATH)
+
+
+class SubversionRepositoryTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.repos = SubversionRepository(REPOS_PATH, None,
+                                          logger_factory('test'))
+
+    def tearDown(self):
+        self.repos = None
+
+    def test_repos_normalize_path(self):
+        self.assertEqual('/', self.repos.normalize_path('/'))
+        self.assertEqual('/', self.repos.normalize_path(''))
+        self.assertEqual('/', self.repos.normalize_path(None))
+        self.assertEqual('trunk', self.repos.normalize_path('trunk'))
+        self.assertEqual('trunk', self.repos.normalize_path('/trunk'))
+        self.assertEqual('trunk', self.repos.normalize_path('trunk/'))
+        self.assertEqual('trunk', self.repos.normalize_path('/trunk/'))
+
+    def test_repos_normalize_rev(self):
+        self.assertEqual(17, self.repos.normalize_rev('latest'))
+        self.assertEqual(17, self.repos.normalize_rev('head'))
+        self.assertEqual(17, self.repos.normalize_rev(''))
+        self.assertEqual(17, self.repos.normalize_rev(None))
+        self.assertEqual(11, self.repos.normalize_rev('11'))
+        self.assertEqual(11, self.repos.normalize_rev(11))
+
+    def test_rev_navigation(self):
+        self.assertEqual(1, self.repos.oldest_rev)
+        self.assertEqual(None, self.repos.previous_rev(0))
+        self.assertEqual(None, self.repos.previous_rev(1))
+        self.assertEqual(17, self.repos.youngest_rev)
+        self.assertEqual(6, self.repos.next_rev(5))
+        self.assertEqual(7, self.repos.next_rev(6))
+        # ...
+        self.assertEqual(None, self.repos.next_rev(17))
+
+    def test_rev_path_navigation(self):
+        self.assertEqual(1, self.repos.oldest_rev)
+        self.assertEqual(None, self.repos.previous_rev(0, 'trunk'))
+        self.assertEqual(None, self.repos.previous_rev(1, 'trunk'))
+        self.assertEqual(17, self.repos.youngest_rev)
+        self.assertEqual(6, self.repos.next_rev(5, 'trunk'))
+        self.assertEqual(13, self.repos.next_rev(6, 'trunk'))
+        # ...
+        self.assertEqual(None, self.repos.next_rev(17, 'trunk'))
+        # test accentuated characters
+        self.assertEqual(None, self.repos.previous_rev(17, u'trunk/R\xe9sum\xe9.txt'))
+        self.assertEqual(17, self.repos.next_rev(16, u'trunk/R\xe9sum\xe9.txt'))
+
+    def test_has_node(self):
+        self.assertEqual(False, self.repos.has_node('/trunk/dir1', 3))
+        self.assertEqual(True, self.repos.has_node('/trunk/dir1', 4))
+        
+    def test_get_node(self):
+        node = self.repos.get_node('/trunk')
+        self.assertEqual('trunk', node.name)
+        self.assertEqual('/trunk', node.path)
+        self.assertEqual(Node.DIRECTORY, node.kind)
+        self.assertEqual(17, node.rev)
+        self.assertEqual(1143808225L, node.last_modified)
+        node = self.repos.get_node('/trunk/README.txt')
+        self.assertEqual('README.txt', node.name)
+        self.assertEqual('/trunk/README.txt', node.path)
+        self.assertEqual(Node.FILE, node.kind)
+        self.assertEqual(3, node.rev)
+        self.assertEqual(1112361898, node.last_modified)
+
+    def test_get_node_specific_rev(self):
+        node = self.repos.get_node('/trunk', 1)
+        self.assertEqual('trunk', node.name)
+        self.assertEqual('/trunk', node.path)
+        self.assertEqual(Node.DIRECTORY, node.kind)
+        self.assertEqual(1, node.rev)
+        self.assertEqual(1112349652, node.last_modified)
+        node = self.repos.get_node('/trunk/README.txt', 2)
+        self.assertEqual('README.txt', node.name)
+        self.assertEqual('/trunk/README.txt', node.path)
+        self.assertEqual(Node.FILE, node.kind)
+        self.assertEqual(2, node.rev)
+        self.assertEqual(1112361138, node.last_modified)
+
+    def test_get_dir_entries(self):
+        node = self.repos.get_node('/trunk')
+        entries = node.get_entries()
+        self.assertEqual(u'R\xe9sum\xe9.txt', entries.next().name)
+        self.assertEqual('dir1', entries.next().name)
+        self.assertEqual('README3.txt', entries.next().name)
+        self.assertEqual('README.txt', entries.next().name)
+        self.assertRaises(StopIteration, entries.next)
+
+    def test_get_file_entries(self):
+        node = self.repos.get_node('/trunk/README.txt')
+        entries = node.get_entries()
+        self.assertRaises(StopIteration, entries.next)
+
+    def test_get_dir_content(self):
+        node = self.repos.get_node('/trunk')
+        self.assertEqual(None, node.content_length)
+        self.assertEqual(None, node.content_type)
+        self.assertEqual(None, node.get_content())
+
+    def test_get_file_content(self):
+        node = self.repos.get_node('/trunk/README.txt')
+        self.assertEqual(8, node.content_length)
+        self.assertEqual('text/plain', node.content_type)
+        self.assertEqual('A test.\n', node.get_content().read())
+
+    def test_get_dir_properties(self):
+        f = self.repos.get_node('/trunk')
+        props = f.get_properties()
+        self.assertEqual(1, len(props))
+
+    def test_get_file_properties(self):
+        f = self.repos.get_node('/trunk/README.txt')
+        props = f.get_properties()
+        self.assertEqual('native', props['svn:eol-style'])
+        self.assertEqual('text/plain', props['svn:mime-type'])
+
+    def test_created_path_rev(self):
+        node = self.repos.get_node('/trunk/README3.txt', 15)
+        self.assertEqual(14, node.rev)
+        self.assertEqual('/trunk/README3.txt', node.path)
+        self.assertEqual(14, node.created_rev)
+        self.assertEqual('trunk/README3.txt', node.created_path)
+
+    def test_created_path_rev_parent_copy(self):
+        node = self.repos.get_node('/tags/v1/README.txt', 15)
+        self.assertEqual(3, node.rev)
+        self.assertEqual('/tags/v1/README.txt', node.path)
+        self.assertEqual(3, node.created_rev)
+        self.assertEqual('trunk/README.txt', node.created_path)
+
+    # Revision Log / node history 
+
+    def test_get_node_history(self):
+        node = self.repos.get_node('/trunk/README3.txt')
+        history = node.get_history()
+        self.assertEqual(('trunk/README3.txt', 14, 'copy'), history.next())
+        self.assertEqual(('trunk/README2.txt', 6, 'copy'), history.next())
+        self.assertEqual(('trunk/README.txt', 3, 'edit'), history.next())
+        self.assertEqual(('trunk/README.txt', 2, 'add'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    def test_get_node_history_follow_copy(self):
+        node = self.repos.get_node('/tags/v1/README.txt')
+        history = node.get_history()
+        self.assertEqual(('tags/v1/README.txt', 7, 'copy'), history.next())
+        self.assertEqual(('trunk/README.txt', 3, 'edit'), history.next())
+        self.assertEqual(('trunk/README.txt', 2, 'add'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    # Revision Log / path history 
+
+    def test_get_path_history(self):
+        history = self.repos.get_path_history('/trunk/README2.txt', None)
+        self.assertEqual(('trunk/README2.txt', 14, 'delete'), history.next())
+        self.assertEqual(('trunk/README2.txt', 6, 'copy'), history.next())
+        self.assertEqual(('trunk/README.txt', 3, 'unknown'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    def test_get_path_history_copied_file(self):
+        history = self.repos.get_path_history('/tags/v1/README.txt', None)
+        self.assertEqual(('tags/v1/README.txt', 7, 'copy'), history.next())
+        self.assertEqual(('trunk/README.txt', 3, 'unknown'), history.next())
+        self.assertRaises(StopIteration, history.next)
+        
+    def test_get_path_history_copied_dir(self):
+        history = self.repos.get_path_history('/branches/v1x', None)
+        self.assertEqual(('branches/v1x', 12, 'copy'), history.next())
+        self.assertEqual(('tags/v1.1', 10, 'unknown'), history.next())
+        self.assertEqual(('branches/v1x', 11, 'delete'), history.next())
+        self.assertEqual(('branches/v1x', 9, 'edit'), history.next())
+        self.assertEqual(('branches/v1x', 8, 'copy'), history.next())
+        self.assertEqual(('tags/v1', 7, 'unknown'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    # Diffs
+
+    def _cmp_diff(self, expected, got):
+        if expected[0]:
+            old = self.repos.get_node(*expected[0])
+            self.assertEqual((old.path, old.rev), (got[0].path, got[0].rev))
+        if expected[1]:
+            new = self.repos.get_node(*expected[1])
+            self.assertEqual((new.path, new.rev), (got[1].path, got[1].rev))
+        self.assertEqual(expected[2], (got[2], got[3]))
+        
+    def test_diff_file_different_revs(self):
+        diffs = self.repos.get_changes('trunk/README.txt', 2, 'trunk/README.txt', 3)
+        self._cmp_diff((('trunk/README.txt', 2),
+                        ('trunk/README.txt', 3),
+                        (Node.FILE, Changeset.EDIT)), diffs.next())
+        self.assertRaises(StopIteration, diffs.next)
+
+    def test_diff_file_different_files(self):
+        diffs = self.repos.get_changes('branches/v1x/README.txt', 12,
+                                      'branches/v1x/README2.txt', 12)
+        self._cmp_diff((('branches/v1x/README.txt', 12),
+                        ('branches/v1x/README2.txt', 12),
+                        (Node.FILE, Changeset.EDIT)), diffs.next())
+        self.assertRaises(StopIteration, diffs.next)
+
+    def test_diff_file_no_change(self):
+        diffs = self.repos.get_changes('trunk/README.txt', 7,
+                                      'tags/v1/README.txt', 7)
+        self.assertRaises(StopIteration, diffs.next)
+ 
+    def test_diff_dir_different_revs(self):
+        diffs = self.repos.get_changes('trunk', 4, 'trunk', 8)
+        self._cmp_diff((None, ('trunk/dir1/dir2', 8),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('trunk/dir1/dir3', 8),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('trunk/README2.txt', 6),
+                        (Node.FILE, Changeset.ADD)), diffs.next())
+        self._cmp_diff((('trunk/dir2', 4), None,
+                        (Node.DIRECTORY, Changeset.DELETE)), diffs.next())
+        self._cmp_diff((('trunk/dir3', 4), None,
+                        (Node.DIRECTORY, Changeset.DELETE)), diffs.next())
+        self.assertRaises(StopIteration, diffs.next)
+
+    def test_diff_dir_different_dirs(self):
+        diffs = self.repos.get_changes('trunk', 1, 'branches/v1x', 12)
+        self._cmp_diff((None, ('branches/v1x/dir1', 12),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('branches/v1x/dir1/dir2', 12),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('branches/v1x/dir1/dir3', 12),
+                        (Node.DIRECTORY, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('branches/v1x/README.txt', 12),
+                        (Node.FILE, Changeset.ADD)), diffs.next())
+        self._cmp_diff((None, ('branches/v1x/README2.txt', 12),
+                        (Node.FILE, Changeset.ADD)), diffs.next())
+        self.assertRaises(StopIteration, diffs.next)
+
+    def test_diff_dir_no_change(self):
+        diffs = self.repos.get_changes('trunk', 7,
+                                      'tags/v1', 7)
+        self.assertRaises(StopIteration, diffs.next)
+        
+    # Changesets
+
+    def test_changeset_repos_creation(self):
+        chgset = self.repos.get_changeset(0)
+        self.assertEqual(0, chgset.rev)
+        self.assertEqual(None, chgset.message)
+        self.assertEqual(None, chgset.author)
+        self.assertEqual(1112349461, chgset.date)
+        self.assertRaises(StopIteration, chgset.get_changes().next)
+
+    def test_changeset_added_dirs(self):
+        chgset = self.repos.get_changeset(1)
+        self.assertEqual(1, chgset.rev)
+        self.assertEqual('Initial directory layout.', chgset.message)
+        self.assertEqual('john', chgset.author)
+        self.assertEqual(1112349652, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('branches', Node.DIRECTORY, Changeset.ADD, None, -1),
+                         changes.next())
+        self.assertEqual(('tags', Node.DIRECTORY, Changeset.ADD, None, -1),
+                         changes.next())
+        self.assertEqual(('trunk', Node.DIRECTORY, Changeset.ADD, None, -1),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_file_edit(self):
+        chgset = self.repos.get_changeset(3)
+        self.assertEqual(3, chgset.rev)
+        self.assertEqual('Fixed README.\n', chgset.message)
+        self.assertEqual('kate', chgset.author)
+        self.assertEqual(1112361898, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('trunk/README.txt', Node.FILE, Changeset.EDIT,
+                          'trunk/README.txt', 2), changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_dir_moves(self):
+        chgset = self.repos.get_changeset(5)
+        self.assertEqual(5, chgset.rev)
+        self.assertEqual('Moved directories.', chgset.message)
+        self.assertEqual('kate', chgset.author)
+        self.assertEqual(1112372739, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('trunk/dir1/dir2', Node.DIRECTORY, Changeset.MOVE,
+                          'trunk/dir2', 4), changes.next())
+        self.assertEqual(('trunk/dir1/dir3', Node.DIRECTORY, Changeset.MOVE,
+                          'trunk/dir3', 4), changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_file_copy(self):
+        chgset = self.repos.get_changeset(6)
+        self.assertEqual(6, chgset.rev)
+        self.assertEqual('More things to read', chgset.message)
+        self.assertEqual('john', chgset.author)
+        self.assertEqual(1112381806, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('trunk/README2.txt', Node.FILE, Changeset.COPY,
+                          'trunk/README.txt', 3), changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_root_propset(self):
+        chgset = self.repos.get_changeset(13)
+        self.assertEqual(13, chgset.rev)
+        self.assertEqual('Setting property on the repository_dir root',
+                         chgset.message)
+        changes = chgset.get_changes()
+        self.assertEqual(('/', Node.DIRECTORY, Changeset.EDIT, '/', 12),
+                         changes.next())
+        self.assertEqual(('trunk', Node.DIRECTORY, Changeset.EDIT, 'trunk', 6),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_base_path_rev(self):
+        chgset = self.repos.get_changeset(9)
+        self.assertEqual(9, chgset.rev)
+        changes = chgset.get_changes()
+        self.assertEqual(('branches/v1x/README.txt', Node.FILE,
+                          Changeset.EDIT, 'trunk/README.txt', 3),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_rename_and_edit(self):
+        chgset = self.repos.get_changeset(14)
+        self.assertEqual(14, chgset.rev)
+        changes = chgset.get_changes()
+        self.assertEqual(('trunk/README3.txt', Node.FILE,
+                          Changeset.MOVE, 'trunk/README2.txt', 13),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_edit_after_wc2wc_copy__original_deleted(self):
+        chgset = self.repos.get_changeset(16)
+        self.assertEqual(16, chgset.rev)
+        changes = chgset.get_changes()
+        self.assertEqual(('branches/v2', Node.DIRECTORY, Changeset.COPY,
+                          'tags/v1.1', 14),
+                         changes.next())
+        self.assertEqual(('branches/v2/README2.txt', Node.FILE,
+                          Changeset.EDIT, 'trunk/README2.txt', 6),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+
+class ScopedSubversionRepositoryTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.repos = SubversionRepository(REPOS_PATH + '/trunk', None,
+                                          logger_factory('test'))
+
+    def tearDown(self):
+        self.repos = None
+
+    def test_repos_normalize_path(self):
+        self.assertEqual('/', self.repos.normalize_path('/'))
+        self.assertEqual('/', self.repos.normalize_path(''))
+        self.assertEqual('/', self.repos.normalize_path(None))
+        self.assertEqual('dir1', self.repos.normalize_path('dir1'))
+        self.assertEqual('dir1', self.repos.normalize_path('/dir1'))
+        self.assertEqual('dir1', self.repos.normalize_path('dir1/'))
+        self.assertEqual('dir1', self.repos.normalize_path('/dir1/'))
+
+    def test_repos_normalize_rev(self):
+        self.assertEqual(17, self.repos.normalize_rev('latest'))
+        self.assertEqual(17, self.repos.normalize_rev('head'))
+        self.assertEqual(17, self.repos.normalize_rev(''))
+        self.assertEqual(17, self.repos.normalize_rev(None))
+        self.assertEqual(5, self.repos.normalize_rev('5'))
+        self.assertEqual(5, self.repos.normalize_rev(5))
+
+    def test_rev_navigation(self):
+        self.assertEqual(1, self.repos.oldest_rev)
+        self.assertEqual(None, self.repos.previous_rev(0))
+        self.assertEqual(1, self.repos.previous_rev(2))
+        self.assertEqual(17, self.repos.youngest_rev)
+        self.assertEqual(2, self.repos.next_rev(1))
+        self.assertEqual(3, self.repos.next_rev(2))
+        # ...
+        self.assertEqual(None, self.repos.next_rev(17))
+
+    def test_has_node(self):
+        self.assertEqual(False, self.repos.has_node('/dir1', 3))
+        self.assertEqual(True, self.repos.has_node('/dir1', 4))
+
+    def test_get_node(self):
+        node = self.repos.get_node('/dir1')
+        self.assertEqual('dir1', node.name)
+        self.assertEqual('/dir1', node.path)
+        self.assertEqual(Node.DIRECTORY, node.kind)
+        self.assertEqual(5, node.rev)
+        self.assertEqual(1112372739, node.last_modified)
+        node = self.repos.get_node('/README.txt')
+        self.assertEqual('README.txt', node.name)
+        self.assertEqual('/README.txt', node.path)
+        self.assertEqual(Node.FILE, node.kind)
+        self.assertEqual(3, node.rev)
+        self.assertEqual(1112361898, node.last_modified)
+
+    def test_get_node_specific_rev(self):
+        node = self.repos.get_node('/dir1', 4)
+        self.assertEqual('dir1', node.name)
+        self.assertEqual('/dir1', node.path)
+        self.assertEqual(Node.DIRECTORY, node.kind)
+        self.assertEqual(4, node.rev)
+        self.assertEqual(1112370155, node.last_modified)
+        node = self.repos.get_node('/README.txt', 2)
+        self.assertEqual('README.txt', node.name)
+        self.assertEqual('/README.txt', node.path)
+        self.assertEqual(Node.FILE, node.kind)
+        self.assertEqual(2, node.rev)
+        self.assertEqual(1112361138, node.last_modified)
+
+    def test_get_dir_entries(self):
+        node = self.repos.get_node('/')
+        entries = node.get_entries()
+        self.assertEqual(u'R\xe9sum\xe9.txt', entries.next().name)
+        self.assertEqual('dir1', entries.next().name)
+        self.assertEqual('README3.txt', entries.next().name)
+        self.assertEqual('README.txt', entries.next().name)
+        self.assertRaises(StopIteration, entries.next)
+
+    def test_get_file_entries(self):
+        node = self.repos.get_node('/README.txt')
+        entries = node.get_entries()
+        self.assertRaises(StopIteration, entries.next)
+
+    def test_get_dir_content(self):
+        node = self.repos.get_node('/dir1')
+        self.assertEqual(None, node.content_length)
+        self.assertEqual(None, node.content_type)
+        self.assertEqual(None, node.get_content())
+
+    def test_get_file_content(self):
+        node = self.repos.get_node('/README.txt')
+        self.assertEqual(8, node.content_length)
+        self.assertEqual('text/plain', node.content_type)
+        self.assertEqual('A test.\n', node.get_content().read())
+
+    def test_get_dir_properties(self):
+        f = self.repos.get_node('/dir1')
+        props = f.get_properties()
+        self.assertEqual(0, len(props))
+
+    def test_get_file_properties(self):
+        f = self.repos.get_node('/README.txt')
+        props = f.get_properties()
+        self.assertEqual('native', props['svn:eol-style'])
+        self.assertEqual('text/plain', props['svn:mime-type'])
+
+    # Revision Log / node history 
+
+    def test_get_node_history(self):
+        node = self.repos.get_node('/README3.txt')
+        history = node.get_history()
+        self.assertEqual(('README3.txt', 14, 'copy'), history.next())
+        self.assertEqual(('README2.txt', 6, 'copy'), history.next())
+        self.assertEqual(('README.txt', 3, 'edit'), history.next())
+        self.assertEqual(('README.txt', 2, 'add'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    def test_get_node_history_follow_copy(self):
+        node = self.repos.get_node('dir1/dir3', )
+        history = node.get_history()
+        self.assertEqual(('dir1/dir3', 5, 'copy'), history.next())
+        self.assertEqual(('dir3', 4, 'add'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    # Revision Log / path history 
+
+    def test_get_path_history(self):
+        history = self.repos.get_path_history('dir3', None)
+        self.assertEqual(('dir3', 5, 'delete'), history.next())
+        self.assertEqual(('dir3', 4, 'add'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    def test_get_path_history_copied_file(self):
+        history = self.repos.get_path_history('README3.txt', None)
+        self.assertEqual(('README3.txt', 14, 'copy'), history.next())
+        self.assertEqual(('README2.txt', 6, 'unknown'), history.next())
+        self.assertRaises(StopIteration, history.next)
+        
+    def test_get_path_history_copied_dir(self):
+        history = self.repos.get_path_history('dir1/dir3', None)
+        self.assertEqual(('dir1/dir3', 5, 'copy'), history.next())
+        self.assertEqual(('dir3', 4, 'unknown'), history.next())
+        self.assertRaises(StopIteration, history.next)
+
+    def test_changeset_repos_creation(self):
+        chgset = self.repos.get_changeset(0)
+        self.assertEqual(0, chgset.rev)
+        self.assertEqual(None, chgset.message)
+        self.assertEqual(None, chgset.author)
+        self.assertEqual(1112349461, chgset.date)
+        self.assertRaises(StopIteration, chgset.get_changes().next)
+
+    def test_changeset_added_dirs(self):
+        chgset = self.repos.get_changeset(4)
+        self.assertEqual(4, chgset.rev)
+        self.assertEqual('More directories.', chgset.message)
+        self.assertEqual('john', chgset.author)
+        self.assertEqual(1112370155, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('dir1', Node.DIRECTORY, 'add', None, -1),
+                         changes.next())
+        self.assertEqual(('dir2', Node.DIRECTORY, 'add', None, -1),
+                         changes.next())
+        self.assertEqual(('dir3', Node.DIRECTORY, 'add', None, -1),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_file_edit(self):
+        chgset = self.repos.get_changeset(3)
+        self.assertEqual(3, chgset.rev)
+        self.assertEqual('Fixed README.\n', chgset.message)
+        self.assertEqual('kate', chgset.author)
+        self.assertEqual(1112361898, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('README.txt', Node.FILE, Changeset.EDIT,
+                          'README.txt', 2), changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_dir_moves(self):
+        chgset = self.repos.get_changeset(5)
+        self.assertEqual(5, chgset.rev)
+        self.assertEqual('Moved directories.', chgset.message)
+        self.assertEqual('kate', chgset.author)
+        self.assertEqual(1112372739, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('dir1/dir2', Node.DIRECTORY, Changeset.MOVE,
+                          'dir2', 4), changes.next())
+        self.assertEqual(('dir1/dir3', Node.DIRECTORY, Changeset.MOVE,
+                          'dir3', 4), changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_file_copy(self):
+        chgset = self.repos.get_changeset(6)
+        self.assertEqual(6, chgset.rev)
+        self.assertEqual('More things to read', chgset.message)
+        self.assertEqual('john', chgset.author)
+        self.assertEqual(1112381806, chgset.date)
+
+        changes = chgset.get_changes()
+        self.assertEqual(('README2.txt', Node.FILE, Changeset.COPY,
+                          'README.txt', 3), changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+    def test_changeset_root_propset(self):
+        chgset = self.repos.get_changeset(13)
+        self.assertEqual(13, chgset.rev)
+        self.assertEqual('Setting property on the repository_dir root',
+                         chgset.message)
+        changes = chgset.get_changes()
+        self.assertEqual(('/', Node.DIRECTORY, Changeset.EDIT, '/', 6),
+                         changes.next())
+        self.assertRaises(StopIteration, changes.next)
+
+
+class RecentPathScopedRepositoryTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.repos = SubversionRepository(REPOS_PATH + '/trunk/dir1', None,
+                                          logger_factory('test'))
+
+    def tearDown(self):
+        self.repos = None
+
+    def test_rev_navigation(self):
+        self.assertEqual(False, self.repos.has_node('/', 1))
+        self.assertEqual(False, self.repos.has_node('/', 2))
+        self.assertEqual(False, self.repos.has_node('/', 3))
+        self.assertEqual(True, self.repos.has_node('/', 4))
+        self.assertEqual(4, self.repos.oldest_rev)
+        self.assertEqual(None, self.repos.previous_rev(4))
+
+
+class NonSelfContainedScopedTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.repos = SubversionRepository(REPOS_PATH + '/tags/v1', None,
+                                          logger_factory('test'))
+
+    def tearDown(self):
+        self.repos = None
+
+    def test_mixed_changeset(self):
+        chgset = self.repos.get_changeset(7)
+        self.assertEqual(7, chgset.rev)
+        changes = chgset.get_changes()
+        self.assertEqual(('/', Node.DIRECTORY, Changeset.ADD, None, -1),
+                         changes.next())
+        self.assertRaises(TracError, lambda: self.repos.get_node(None, 6))
+
+
+class AnotherNonSelfContainedScopedTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.repos = SubversionRepository(REPOS_PATH + '/branches', None,
+                                          logger_factory('test'))
+
+    def tearDown(self):
+        self.repos = None
+
+    def test_mixed_changeset_with_edit(self):
+        chgset = self.repos.get_changeset(9)
+        self.assertEqual(9, chgset.rev)
+        changes = chgset.get_changes()
+        self.assertEqual(('v1x/README.txt', Node.FILE, Changeset.EDIT,
+                          'v1x/README.txt', 8),
+                         changes.next())
+
+
+def suite():
+    global has_svn
+    suite = unittest.TestSuite()
+    if has_svn:
+        suite.addTest(unittest.makeSuite(SubversionRepositoryTestCase,
+            'test', suiteClass=SubversionRepositoryTestSetup))
+        suite.addTest(unittest.makeSuite(ScopedSubversionRepositoryTestCase,
+            'test', suiteClass=SubversionRepositoryTestSetup))
+        suite.addTest(unittest.makeSuite(RecentPathScopedRepositoryTestCase,
+            'test', suiteClass=SubversionRepositoryTestSetup))
+        suite.addTest(unittest.makeSuite(NonSelfContainedScopedTestCase,
+            'test', suiteClass=SubversionRepositoryTestSetup))
+        suite.addTest(unittest.makeSuite(AnotherNonSelfContainedScopedTestCase,
+            'test', suiteClass=SubversionRepositoryTestSetup))
+    return suite
+
+if __name__ == '__main__':
+    runner = unittest.TextTestRunner()
+    runner.run(suite())
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/tests/svnrepos.dump
@@ -0,0 +1,554 @@
+SVN-fs-dump-format-version: 2
+
+UUID: 92ea810a-adf3-0310-b540-bef912dcf5ba
+
+Revision-number: 0
+Prop-content-length: 56
+Content-length: 56
+
+K 8
+svn:date
+V 27
+2005-04-01T09:57:41.312767Z
+PROPS-END
+
+Revision-number: 1
+Prop-content-length: 124
+Content-length: 124
+
+K 7
+svn:log
+V 25
+Initial directory layout.
+K 10
+svn:author
+V 4
+john
+K 8
+svn:date
+V 27
+2005-04-01T10:00:52.353248Z
+PROPS-END
+
+Node-path: branches
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: tags
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 2
+Prop-content-length: 112
+Content-length: 112
+
+K 7
+svn:log
+V 13
+Added README.
+K 10
+svn:author
+V 4
+john
+K 8
+svn:date
+V 27
+2005-04-01T13:12:18.216267Z
+PROPS-END
+
+Node-path: trunk/README.txt
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 8
+Text-content-md5: a0691c0f61f52683bcb05da98fe028c8
+Content-length: 18
+
+PROPS-END
+A text.
+
+
+Revision-number: 3
+Prop-content-length: 113
+Content-length: 113
+
+K 7
+svn:log
+V 14
+Fixed README.
+
+K 10
+svn:author
+V 4
+kate
+K 8
+svn:date
+V 27
+2005-04-01T13:24:58.234643Z
+PROPS-END
+
+Node-path: trunk/README.txt
+Node-kind: file
+Node-action: change
+Prop-content-length: 75
+Text-content-length: 8
+Text-content-md5: eaf1c95c78c9f848636d357788bd4a4c
+Content-length: 83
+
+K 13
+svn:mime-type
+V 10
+text/plain
+K 13
+svn:eol-style
+V 6
+native
+PROPS-END
+A test.
+
+
+Revision-number: 4
+Prop-content-length: 116
+Content-length: 116
+
+K 7
+svn:log
+V 17
+More directories.
+K 10
+svn:author
+V 4
+john
+K 8
+svn:date
+V 27
+2005-04-01T15:42:35.450595Z
+PROPS-END
+
+Node-path: trunk/dir1
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk/dir2
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Node-path: trunk/dir3
+Node-kind: dir
+Node-action: add
+Prop-content-length: 10
+Content-length: 10
+
+PROPS-END
+
+
+Revision-number: 5
+Prop-content-length: 117
+Content-length: 117
+
+K 7
+svn:log
+V 18
+Moved directories.
+K 10
+svn:author
+V 4
+kate
+K 8
+svn:date
+V 27
+2005-04-01T16:25:39.658099Z
+PROPS-END
+
+Node-path: trunk/dir1/dir2
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 4
+Node-copyfrom-path: trunk/dir2
+
+
+Node-path: trunk/dir1/dir3
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 4
+Node-copyfrom-path: trunk/dir3
+
+
+Node-path: trunk/dir2
+Node-action: delete
+
+
+Node-path: trunk/dir3
+Node-action: delete
+
+
+Revision-number: 6
+Prop-content-length: 118
+Content-length: 118
+
+K 7
+svn:log
+V 19
+More things to read
+K 10
+svn:author
+V 4
+john
+K 8
+svn:date
+V 27
+2005-04-01T18:56:46.985846Z
+PROPS-END
+
+Node-path: trunk/README2.txt
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 3
+Node-copyfrom-path: trunk/README.txt
+
+
+Revision-number: 7
+Prop-content-length: 151
+Content-length: 151
+
+K 7
+svn:log
+V 42
+test the tag operation (copy of directory)
+K 10
+svn:author
+V 13
+Administrator
+K 8
+svn:date
+V 27
+2005-04-14T15:06:20.717616Z
+PROPS-END
+
+Node-path: tags/v1
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 6
+Node-copyfrom-path: trunk
+
+
+Revision-number: 8
+Prop-content-length: 124
+Content-length: 124
+
+K 7
+svn:log
+V 15
+Fix stuff in v1
+K 10
+svn:author
+V 13
+Administrator
+K 8
+svn:date
+V 27
+2005-04-22T08:57:33.499643Z
+PROPS-END
+
+Node-path: branches/v1x
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 7
+Node-copyfrom-path: tags/v1
+
+
+Revision-number: 9
+Prop-content-length: 127
+Content-length: 127
+
+K 7
+svn:log
+V 18
+Now that's the fix
+K 10
+svn:author
+V 13
+Administrator
+K 8
+svn:date
+V 27
+2005-04-22T08:59:24.308979Z
+PROPS-END
+
+Node-path: branches/v1x/README.txt
+Node-kind: file
+Node-action: change
+Text-content-length: 16
+Text-content-md5: 02bcabffffd16fe0fc250f08cad95e0c
+Content-length: 16
+
+This is a test.
+
+
+Revision-number: 10
+Prop-content-length: 141
+Content-length: 141
+
+K 7
+svn:log
+V 32
+Tagging v1.1 from the fix branch
+K 10
+svn:author
+V 13
+Administrator
+K 8
+svn:date
+V 27
+2005-04-22T09:00:34.549980Z
+PROPS-END
+
+Node-path: tags/v1.1
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 9
+Node-copyfrom-path: branches/v1x
+
+
+Revision-number: 11
+Prop-content-length: 191
+Content-length: 191
+
+K 7
+svn:log
+V 82
+''(a few months later)'' We don't need the fix branch anymore, 1.1 is super-stable
+K 10
+svn:author
+V 13
+Administrator
+K 8
+svn:date
+V 27
+2005-04-22T09:01:38.361737Z
+PROPS-END
+
+Node-path: branches/v1x
+Node-action: delete
+
+
+Revision-number: 12
+Prop-content-length: 166
+Content-length: 166
+
+K 7
+svn:log
+V 57
+''(a few years later)'' Argh... v1.1 was buggy, after all
+K 10
+svn:author
+V 13
+Administrator
+K 8
+svn:date
+V 27
+2005-04-22T09:06:37.011174Z
+PROPS-END
+
+Node-path: branches/v1x
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 11
+Node-copyfrom-path: tags/v1.1
+
+
+Revision-number: 13
+Prop-content-length: 143
+Content-length: 143
+
+K 7
+svn:log
+V 43
+Setting property on the repository_dir root
+K 10
+svn:author
+V 5
+cboos
+K 8
+svn:date
+V 27
+2005-11-17T15:13:16.197772Z
+PROPS-END
+
+Node-path: 
+Node-kind: dir
+Node-action: change
+Prop-content-length: 37
+Content-length: 37
+
+K 10
+svn:ignore
+V 6
+*.pyc
+
+PROPS-END
+
+
+Node-path: trunk
+Node-kind: dir
+Node-action: change
+Prop-content-length: 37
+Content-length: 37
+
+K 10
+svn:ignore
+V 6
+*.pyc
+
+PROPS-END
+
+
+Revision-number: 14
+Prop-content-length: 119
+Content-length: 119
+
+K 7
+svn:log
+V 19
+Testing rename+edit
+K 10
+svn:author
+V 5
+cboos
+K 8
+svn:date
+V 27
+2005-11-30T08:47:03.467814Z
+PROPS-END
+
+Node-path: trunk/README3.txt
+Node-kind: file
+Node-action: add
+Node-copyfrom-rev: 13
+Node-copyfrom-path: trunk/README2.txt
+Text-content-length: 42
+Text-content-md5: 211b820b566541dd49a1283d6476d89f
+Content-length: 42
+
+A test of svn move, followed by a change.
+
+
+Node-path: trunk/README2.txt
+Node-action: delete
+
+
+Revision-number: 15
+Prop-content-length: 162
+Content-length: 162
+
+K 7
+svn:log
+V 62
+Removing original file, just before committing the wc->wc copy
+K 10
+svn:author
+V 5
+cboos
+K 8
+svn:date
+V 27
+2005-12-06T12:47:36.271020Z
+PROPS-END
+
+Node-path: tags/v1.1/README2.txt
+Node-action: delete
+
+
+Revision-number: 16
+Prop-content-length: 138
+Content-length: 138
+
+K 7
+svn:log
+V 38
+Committing wc->wc copy + local changes
+K 10
+svn:author
+V 5
+cboos
+K 8
+svn:date
+V 27
+2005-12-06T12:47:51.122376Z
+PROPS-END
+
+Node-path: branches/v2
+Node-kind: dir
+Node-action: add
+Node-copyfrom-rev: 14
+Node-copyfrom-path: tags/v1.1
+
+
+Node-path: branches/v2/README2.txt
+Node-kind: file
+Node-action: change
+Text-content-length: 47
+Text-content-md5: 9b7bad978c6ad159f939c3db2038cbb1
+Content-length: 47
+
+A test + local modifications after wc->wc copy
+
+
+Revision-number: 17
+Prop-content-length: 139
+Content-length: 139
+
+K 7
+svn:log
+V 39
+Test des caractères accentués (cp437)
+K 10
+svn:author
+V 5
+cboos
+K 8
+svn:date
+V 27
+2006-03-31T12:30:25.421875Z
+PROPS-END
+
+Node-path: trunk/Résumé.txt
+Node-kind: file
+Node-action: add
+Prop-content-length: 10
+Text-content-length: 13
+Text-content-md5: 858e52306ecdfcb6f6eadb50d6f9086b
+Content-length: 23
+
+PROPS-END
+En résumé ...
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/web_ui/__init__.py
@@ -0,0 +1,4 @@
+from trac.versioncontrol.web_ui.browser import *
+from trac.versioncontrol.web_ui.changeset import *
+from trac.versioncontrol.web_ui.log import *
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/web_ui/browser.py
@@ -0,0 +1,286 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+
+import re
+import urllib
+import os.path
+from fnmatch import fnmatchcase
+
+from trac import util
+from trac.config import ListOption, Option
+from trac.core import *
+from trac.mimeview import Mimeview, is_binary, get_mimetype
+from trac.perm import IPermissionRequestor
+from trac.util import sorted, embedded_numbers
+from trac.util.datefmt import http_date, format_datetime, pretty_timedelta
+from trac.util.markup import escape, html, Markup
+from trac.util.text import pretty_size
+from trac.web import IRequestHandler, RequestDone
+from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
+from trac.wiki import wiki_to_html, IWikiSyntaxProvider
+from trac.versioncontrol.api import NoSuchChangeset
+from trac.versioncontrol.web_ui.util import *
+
+
+CHUNK_SIZE = 4096
+
+
+class BrowserModule(Component):
+
+    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
+               IWikiSyntaxProvider)
+
+    hidden_properties = Option('browser', 'hide_properties', 'svk:merge',
+        """List of subversion properties to hide from the repository browser
+        (''since 0.9'')""")
+
+    downloadable_paths = ListOption('browser', 'downloadable_paths',
+                                    '/trunk, /branches/*, /tags/*', doc=
+        """List of repository paths that can be downloaded.
+        
+        Leave the option empty if you want to disable all downloads, otherwise
+        set it to a comma-separated list of authorized paths (those paths are
+        glob patterns, i.e. "*" can be used as a wild card)
+        (''since 0.10'')""")
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'browser'
+
+    def get_navigation_items(self, req):
+        if not req.perm.has_permission('BROWSER_VIEW'):
+            return
+        yield ('mainnav', 'browser',
+               html.A('Browse Source', href=req.href.browser()))
+
+    # IPermissionRequestor methods
+
+    def get_permission_actions(self):
+        return ['BROWSER_VIEW', 'FILE_VIEW']
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        import re
+        match = re.match(r'/(browser|file)(?:(/.*))?', req.path_info)
+        if match:
+            req.args['path'] = match.group(2) or '/'
+            if match.group(1) == 'file':
+                req.redirect(req.href.browser(req.args.get('path'),
+                                              rev=req.args.get('rev'),
+                                              format=req.args.get('format')),
+                             permanent=True)
+            return True
+
+    def process_request(self, req):
+        path = req.args.get('path', '/')
+        rev = req.args.get('rev') or None
+
+        # Find node for the requested path/rev
+        repos = self.env.get_repository(req.authname)
+        if rev:
+            rev = repos.normalize_rev(rev)
+        # If `rev` is `None`, we'll try to reuse `None` consistently,
+        # as a special shortcut to the latest revision.
+        rev_or_latest = rev or repos.youngest_rev
+        node = get_existing_node(req, repos, path, rev_or_latest)
+
+        # Rendered list of node properties
+        hidden_properties = self.hidden_properties
+        properties = []
+        for name, value in node.get_properties().items():
+            if not name in hidden_properties:
+                properties.append({
+                    'name': name,
+                    'value': render_node_property(self.env, name, value)})
+
+        req.hdf['title'] = path
+        req.hdf['browser'] = {
+            'path': path,
+            'revision': rev,
+            'props': properties,
+            'href': req.href.browser(path, rev=rev),
+            'log_href': req.href.log(path, rev=rev),
+            'restr_changeset_href': req.href.changeset(node.rev,
+                                                       node.created_path),
+            'anydiff_href': req.href.anydiff(),
+        }
+
+        path_links = get_path_links(req.href, path, rev)
+        if len(path_links) > 1:
+            add_link(req, 'up', path_links[-2]['href'], 'Parent directory')
+        req.hdf['browser.path'] = path_links
+
+        if node.isdir:
+            req.hdf['browser.is_dir'] = True
+            self._render_directory(req, repos, node, rev)
+        else:
+            self._render_file(req, repos, node, rev)
+
+        add_stylesheet(req, 'common/css/browser.css')
+        return 'browser.cs', None
+
+    # Internal methods
+
+    def _render_directory(self, req, repos, node, rev=None):
+        req.perm.assert_permission('BROWSER_VIEW')
+
+        # Entries metadata
+        info = []
+        for entry in node.get_entries():
+            info.append({
+                'name': entry.name,
+                'fullpath': entry.path,
+                'is_dir': entry.isdir,
+                'content_length': entry.content_length,
+                'size': pretty_size(entry.content_length),
+                'rev': entry.rev,
+                'permission': 1, # FIXME
+                'log_href': req.href.log(entry.path, rev=rev),
+                'browser_href': req.href.browser(entry.path, rev=rev)
+            })
+        changes = get_changes(self.env, repos, [i['rev'] for i in info])
+
+        # Ordering of entries
+        order = req.args.get('order', 'name').lower()
+        desc = req.args.has_key('desc')
+
+        if order == 'date':
+            def file_order(a):
+                return changes[a['rev']]['date_seconds']
+        elif order == 'size':
+            def file_order(a):
+                return (a['content_length'],
+                        embedded_numbers(a['name'].lower()))
+        else:
+            def file_order(a):
+                return embedded_numbers(a['name'].lower())
+
+        dir_order = desc and 1 or -1
+
+        def browse_order(a):
+            return a['is_dir'] and dir_order or 0, file_order(a)
+        info = sorted(info, key=browse_order, reverse=desc)
+
+        switch_ordering_hrefs = {}
+        for col in ('name', 'size', 'date'):
+            switch_ordering_hrefs[col] = req.href.browser(
+                node.path, rev=rev, order=col,
+                desc=(col == order and not desc and 1 or None))
+
+        # ''Zip Archive'' alternate link
+        patterns = self.downloadable_paths
+        if node.path and patterns and \
+               filter(None, [fnmatchcase(node.path, p) for p in patterns]):
+            zip_href = req.href.changeset(rev or repos.youngest_rev, node.path,
+                                          old=rev, old_path='/', format='zip')
+            add_link(req, 'alternate', zip_href, 'Zip Archive',
+                     'application/zip', 'zip')
+
+        req.hdf['browser'] = {'order': order, 'desc': desc and 1 or 0,
+                              'items': info, 'changes': changes,
+                              'order_href': switch_ordering_hrefs}
+
+    def _render_file(self, req, repos, node, rev=None):
+        req.perm.assert_permission('FILE_VIEW')
+
+        mimeview = Mimeview(self.env)
+
+        # MIME type detection
+        content = node.get_content()
+        chunk = content.read(CHUNK_SIZE)
+        mime_type = node.content_type
+        if not mime_type or mime_type == 'application/octet-stream':
+            mime_type = mimeview.get_mimetype(node.name, chunk) or \
+                        mime_type or 'text/plain'
+
+        # Eventually send the file directly
+        format = req.args.get('format')
+        if format in ['raw', 'txt']:
+            req.send_response(200)
+            req.send_header('Content-Type',
+                            format == 'txt' and 'text/plain' or mime_type)
+            req.send_header('Content-Length', node.content_length)
+            req.send_header('Last-Modified', http_date(node.last_modified))
+            req.end_headers()
+
+            while 1:
+                if not chunk:
+                    raise RequestDone
+                req.write(chunk)
+                chunk = content.read(CHUNK_SIZE)
+        else:
+            # The changeset corresponding to the last change on `node` 
+            # is more interesting than the `rev` changeset.
+            changeset = repos.get_changeset(node.rev)
+
+            message = changeset.message or '--'
+            if self.config['changeset'].getbool('wiki_format_messages'):
+                message = wiki_to_html(message, self.env, req,
+                                       escape_newlines=True)
+            else:
+                message = html.PRE(message)
+
+            req.hdf['file'] = {
+                'rev': node.rev,
+                'changeset_href': req.href.changeset(node.rev),
+                'date': format_datetime(changeset.date),
+                'age': pretty_timedelta(changeset.date),
+                'size': pretty_size(node.content_length),
+                'author': changeset.author or 'anonymous',
+                'message': message
+            } 
+
+            # add ''Plain Text'' alternate link if needed
+            if not is_binary(chunk) and mime_type != 'text/plain':
+                plain_href = req.href.browser(node.path, rev=rev, format='txt')
+                add_link(req, 'alternate', plain_href, 'Plain Text',
+                         'text/plain')
+
+            # add ''Original Format'' alternate link (always)
+            raw_href = req.href.browser(node.path, rev=rev, format='raw')
+            add_link(req, 'alternate', raw_href, 'Original Format', mime_type)
+
+            self.log.debug("Rendering preview of node %s@%s with mime-type %s"
+                           % (node.name, str(rev), mime_type))
+
+            del content # the remainder of that content is not needed
+
+            req.hdf['file'] = mimeview.preview_to_hdf(
+                req, node.get_content(), node.get_content_length(), mime_type,
+                node.created_path, raw_href, annotations=['lineno'])
+
+            add_stylesheet(req, 'common/css/code.css')
+
+    # IWikiSyntaxProvider methods
+
+    def get_wiki_syntax(self):
+        return []
+
+    def get_link_resolvers(self):
+        return [('repos', self._format_link),
+                ('source', self._format_link),
+                ('browser', self._format_link)]
+
+    def _format_link(self, formatter, ns, path, label):
+        path, rev, line = get_path_rev_line(path)
+        fragment = ''
+        if line is not None:
+            fragment = '#L%d' % line
+        return html.A(label, class_='source',
+                      href=formatter.href.browser(path, rev=rev) + fragment)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/web_ui/changeset.py
@@ -0,0 +1,808 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2004-2006 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+#         Christian Boos <cboos@neuf.fr>
+
+import posixpath
+import re
+from StringIO import StringIO
+import time
+
+from trac import util
+from trac.config import BoolOption, IntOption
+from trac.core import *
+from trac.mimeview import Mimeview, is_binary
+from trac.perm import IPermissionRequestor
+from trac.Search import ISearchSource, search_to_sql, shorten_result
+from trac.Timeline import ITimelineEventProvider
+from trac.util.datefmt import format_datetime, pretty_timedelta
+from trac.util.markup import html, escape, unescape, Markup
+from trac.util.text import unicode_urlencode, shorten_line, CRLF
+from trac.versioncontrol import Changeset, Node
+from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff
+from trac.versioncontrol.svn_authz import SubversionAuthorizer
+from trac.versioncontrol.web_ui.util import render_node_property
+from trac.web import IRequestHandler
+from trac.web.chrome import INavigationContributor, add_link, add_stylesheet
+from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider, \
+                      Formatter
+
+
+class DiffArgs(dict):
+    def __getattr__(self, str):
+        return self[str]
+
+
+class ChangesetModule(Component):
+    """Provide flexible functionality for showing sets of differences.
+
+    If the differences shown are coming from a specific changeset,
+    then that changeset informations can be shown too.
+
+    In addition, it is possible to show only a subset of the changeset:
+    Only the changes affecting a given path will be shown.
+    This is called the ''restricted'' changeset.
+
+    But the differences can also be computed in a more general way,
+    between two arbitrary paths and/or between two arbitrary revisions.
+    In that case, there's no changeset information displayed.
+    """
+
+    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
+               ITimelineEventProvider, IWikiSyntaxProvider, ISearchSource)
+
+    timeline_show_files = IntOption('timeline', 'changeset_show_files', 0,
+        """Number of files to show (`-1` for unlimited, `0` to disable).""")
+
+    timeline_long_messages = BoolOption('timeline', 'changeset_long_messages',
+                                        'false',
+        """Whether wiki-formatted changeset messages should be multiline or not.
+
+        If this option is not specified or is false and `wiki_format_messages`
+        is set to true, changeset messages will be single line only, losing
+        some formatting (bullet points, etc).""")
+
+    max_diff_files = IntOption('changeset', 'max_diff_files', 0,
+        """Maximum number of modified files for which the changeset view will
+        attempt to show the diffs inlined (''since 0.10'')."""),
+
+    max_diff_bytes = IntOption('changeset', 'max_diff_bytes', 10000000,
+        """Maximum total size in bytes of the modified files (their old size
+        plus their new size) for which the changeset view will attempt to show
+        the diffs inlined (''since 0.10'').""")
+
+    wiki_format_messages = BoolOption('changeset', 'wiki_format_messages',
+                                      'true',
+        """Whether wiki formatting should be applied to changeset messages.
+        
+        If this option is disabled, changeset messages will be rendered as
+        pre-formatted text.""")
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'browser'
+
+    def get_navigation_items(self, req):
+        return []
+
+    # IPermissionRequestor methods
+
+    def get_permission_actions(self):
+        return ['CHANGESET_VIEW']
+
+    # IRequestHandler methods
+
+    _request_re = re.compile(r"/changeset(?:/([^/]+))?(/.*)?$")
+
+    def match_request(self, req):
+        match = re.match(self._request_re, req.path_info)
+        if match:
+            new, new_path = match.groups()
+            if new:
+                req.args['new'] = new
+            if new_path:
+                req.args['new_path'] = new_path
+            return True
+
+    def process_request(self, req):
+        """The appropriate mode of operation is inferred from the request
+        parameters:
+
+         * If `new_path` and `old_path` are equal (or `old_path` is omitted)
+           and `new` and `old` are equal (or `old` is omitted),
+           then we're about to view a revision Changeset: `chgset` is True.
+           Furthermore, if the path is not the root, the changeset is
+           ''restricted'' to that path (only the changes affecting that path,
+           its children or its ancestor directories will be shown).
+         * In any other case, the set of changes corresponds to arbitrary
+           differences between path@rev pairs. If `new_path` and `old_path`
+           are equal, the ''restricted'' flag will also be set, meaning in this
+           case that the differences between two revisions are restricted to
+           those occurring on that path.
+
+        In any case, either path@rev pairs must exist.
+        """
+        req.perm.assert_permission('CHANGESET_VIEW')
+
+        # -- retrieve arguments
+        new_path = req.args.get('new_path')
+        new = req.args.get('new')
+        old_path = req.args.get('old_path')
+        old = req.args.get('old')
+
+        if old and '@' in old:
+            old_path, old = unescape(old).split('@')
+        if new and '@' in new:
+            new_path, new = unescape(new).split('@')
+
+        # -- normalize and check for special case
+        repos = self.env.get_repository(req.authname)
+        new_path = repos.normalize_path(new_path)
+        new = repos.normalize_rev(new)
+        old_path = repos.normalize_path(old_path or new_path)
+        old = repos.normalize_rev(old or new)
+
+        authzperm = SubversionAuthorizer(self.env, req.authname)
+        authzperm.assert_permission_for_changeset(new)
+
+        if old_path == new_path and old == new: # revert to Changeset
+            old_path = old = None
+
+        diff_options = get_diff_options(req)
+
+        # -- setup the `chgset` and `restricted` flags, see docstring above.
+        chgset = not old and not old_path
+        if chgset:
+            restricted = new_path not in ('', '/') # (subset or not)
+        else:
+            restricted = old_path == new_path # (same path or not)
+
+        # -- redirect if changing the diff options
+        if req.args.has_key('update'):
+            if chgset:
+                if restricted:
+                    req.redirect(req.href.changeset(new, new_path))
+                else:
+                    req.redirect(req.href.changeset(new))
+            else:
+                req.redirect(req.href.changeset(new, new_path, old=old,
+                                                old_path=old_path))
+
+        # -- preparing the diff arguments
+        if chgset:
+            prev = repos.get_node(new_path, new).get_previous()
+            if prev:
+                prev_path, prev_rev = prev[:2]
+            else:
+                prev_path, prev_rev = new_path, repos.previous_rev(new)
+            diff_args = DiffArgs(old_path=prev_path, old_rev=prev_rev,
+                                 new_path=new_path, new_rev=new)
+        else:
+            if not new:
+                new = repos.youngest_rev
+            elif not old:
+                old = repos.youngest_rev
+            if not old_path:
+                old_path = new_path
+            diff_args = DiffArgs(old_path=old_path, old_rev=old,
+                                 new_path=new_path, new_rev=new)
+        if chgset:
+            chgset = repos.get_changeset(new)
+            message = chgset.message or '--'
+            if self.wiki_format_messages:
+                message = wiki_to_html(message, self.env, req,
+                                              escape_newlines=True)
+            else:
+                message = html.PRE(message)
+            req.check_modified(chgset.date, [
+                diff_options[0],
+                ''.join(diff_options[1]),
+                repos.name,
+                repos.rev_older_than(new, repos.youngest_rev),
+                message,
+                pretty_timedelta(chgset.date, None, 3600)])
+        else:
+            message = None # FIXME: what date should we choose for a diff?
+
+        req.hdf['changeset'] = diff_args
+
+        format = req.args.get('format')
+
+        if format in ['diff', 'zip']:
+            req.perm.assert_permission('FILE_VIEW')
+            # choosing an appropriate filename
+            rpath = new_path.replace('/','_')
+            if chgset:
+                if restricted:
+                    filename = 'changeset_%s_r%s' % (rpath, new)
+                else:
+                    filename = 'changeset_r%s' % new
+            else:
+                if restricted:
+                    filename = 'diff-%s-from-r%s-to-r%s' \
+                                  % (rpath, old, new)
+                elif old_path == '/': # special case for download (#238)
+                    filename = '%s-r%s' % (rpath, old)
+                else:
+                    filename = 'diff-from-%s-r%s-to-%s-r%s' \
+                               % (old_path.replace('/','_'), old, rpath, new)
+            if format == 'diff':
+                self._render_diff(req, filename, repos, diff_args,
+                                  diff_options)
+                return
+            elif format == 'zip':
+                self._render_zip(req, filename, repos, diff_args)
+                return
+
+        # -- HTML format
+        self._render_html(req, repos, chgset, restricted, message,
+                          diff_args, diff_options)
+        if chgset:
+            diff_params = 'new=%s' % new
+        else:
+            diff_params = unicode_urlencode({'new_path': new_path,
+                                             'new': new,
+                                             'old_path': old_path,
+                                             'old': old})
+        add_link(req, 'alternate', '?format=diff&'+diff_params, 'Unified Diff',
+                 'text/plain', 'diff')
+        add_link(req, 'alternate', '?format=zip&'+diff_params, 'Zip Archive',
+                 'application/zip', 'zip')
+        add_stylesheet(req, 'common/css/changeset.css')
+        add_stylesheet(req, 'common/css/diff.css')
+        add_stylesheet(req, 'common/css/code.css')
+        return 'changeset.cs', None
+
+    # Internal methods
+
+    def _render_html(self, req, repos, chgset, restricted, message,
+                     diff, diff_options):
+        """HTML version"""
+        req.hdf['changeset'] = {
+            'chgset': chgset and True,
+            'restricted': restricted,
+            'href': {
+                'new_rev': req.href.changeset(diff.new_rev),
+                'old_rev': req.href.changeset(diff.old_rev),
+                'new_path': req.href.browser(diff.new_path, rev=diff.new_rev),
+                'old_path': req.href.browser(diff.old_path, rev=diff.old_rev)
+            }
+        }
+
+        if chgset: # Changeset Mode (possibly restricted on a path)
+            path, rev = diff.new_path, diff.new_rev
+
+            # -- getting the change summary from the Changeset.get_changes
+            def get_changes():
+                for npath, kind, change, opath, orev in chgset.get_changes():
+                    old_node = new_node = None
+                    if (restricted and
+                        not (npath == path or                # same path
+                             npath.startswith(path + '/') or # npath is below
+                             path.startswith(npath + '/'))): # npath is above
+                        continue
+                    if change != Changeset.ADD:
+                        old_node = repos.get_node(opath, orev)
+                    if change != Changeset.DELETE:
+                        new_node = repos.get_node(npath, rev)
+                    yield old_node, new_node, kind, change
+
+            def _changeset_title(rev):
+                if restricted:
+                    return 'Changeset %s for %s' % (rev, path)
+                else:
+                    return 'Changeset %s' % rev
+
+            title = _changeset_title(rev)
+            properties = []
+            for name, value, wikiflag, htmlclass in chgset.get_properties():
+                if wikiflag:
+                    value = wiki_to_html(value or '', self.env, req)
+                properties.append({'name': name, 'value': value,
+                                   'htmlclass': htmlclass})
+
+            req.hdf['changeset'] = {
+                'revision': chgset.rev,
+                'time': format_datetime(chgset.date),
+                'age': pretty_timedelta(chgset.date, None, 3600),
+                'author': chgset.author or 'anonymous',
+                'message': message, 'properties': properties
+            }
+            oldest_rev = repos.oldest_rev
+            if chgset.rev != oldest_rev:
+                if restricted:
+                    prev = repos.get_node(path, rev).get_previous()
+                    if prev:
+                        prev_path, prev_rev = prev[:2]
+                        if prev_rev:
+                            prev_href = req.href.changeset(prev_rev, prev_path)
+                    else:
+                        prev_path = prev_rev = None
+                else:
+                    add_link(req, 'first', req.href.changeset(oldest_rev),
+                             'Changeset %s' % oldest_rev)
+                    prev_path = diff.old_path
+                    prev_rev = repos.previous_rev(chgset.rev)
+                    if prev_rev:
+                        prev_href = req.href.changeset(prev_rev)
+                if prev_rev:
+                    add_link(req, 'prev', prev_href, _changeset_title(prev_rev))
+            youngest_rev = repos.youngest_rev
+            if str(chgset.rev) != str(youngest_rev):
+                if restricted:
+                    next_rev = repos.next_rev(chgset.rev, path)
+                    if next_rev:
+                        next_href = req.href.changeset(next_rev, path)
+                else:
+                    add_link(req, 'last', req.href.changeset(youngest_rev),
+                             'Changeset %s' % youngest_rev)
+                    next_rev = repos.next_rev(chgset.rev)
+                    if next_rev:
+                        next_href = req.href.changeset(next_rev)
+                if next_rev:
+                    add_link(req, 'next', next_href, _changeset_title(next_rev))
+
+        else: # Diff Mode
+            # -- getting the change summary from the Repository.get_changes
+            def get_changes():
+                for d in repos.get_changes(**diff):
+                    yield d
+
+            reverse_href = req.href.changeset(diff.old_rev, diff.old_path,
+                                                   old=diff.new_rev,
+                                                   old_path=diff.new_path)
+            req.hdf['changeset.reverse_href'] = reverse_href
+            req.hdf['changeset.href.log'] = req.href.log(
+                diff.new_path, rev=diff.new_rev, stop_rev=diff.old_rev)
+            title = self.title_for_diff(diff)
+        req.hdf['title'] = title
+
+        if not req.perm.has_permission('BROWSER_VIEW'):
+            return
+
+        def _change_info(old_node, new_node, change):
+            info = {'change': change}
+            if old_node:
+                info['path.old'] = old_node.path
+                info['rev.old'] = old_node.rev
+                info['shortrev.old'] = repos.short_rev(old_node.rev)
+                old_href = req.href.browser(old_node.created_path,
+                                            rev=old_node.created_rev)
+                # Reminder: old_node.path may not exist at old_node.rev
+                #           as long as old_node.rev==old_node.created_rev
+                #           ... and diff.old_rev may have nothing to do
+                #           with _that_ node specific history...
+                info['browser_href.old'] = old_href
+            if new_node:
+                info['path.new'] = new_node.path
+                info['rev.new'] = new_node.rev # created rev.
+                info['shortrev.new'] = repos.short_rev(new_node.rev)
+                new_href = req.href.browser(new_node.created_path,
+                                            rev=new_node.created_rev)
+                # (same remark as above)
+                info['browser_href.new'] = new_href
+            return info
+
+        hidden_properties = self.config.getlist('browser', 'hide_properties')
+
+        def _prop_changes(old_node, new_node):
+            old_props = old_node.get_properties()
+            new_props = new_node.get_properties()
+            changed_props = {}
+            if old_props != new_props:
+                for k,v in old_props.items():
+                    if not k in new_props:
+                        changed_props[k] = {
+                            'old': render_node_property(self.env, k, v)}
+                    elif v != new_props[k]:
+                        changed_props[k] = {
+                            'old': render_node_property(self.env, k, v),
+                            'new': render_node_property(self.env, k,
+                                                        new_props[k])}
+                for k,v in new_props.items():
+                    if not k in old_props:
+                        changed_props[k] = {
+                            'new': render_node_property(self.env, k, v)}
+                for k in hidden_properties:
+                    if k in changed_props:
+                        del changed_props[k]
+            changed_properties = []
+            for name, props in changed_props.iteritems():
+                props.update({'name': name})
+                changed_properties.append(props)
+            return changed_properties
+
+        def _estimate_changes(old_node, new_node):
+            old_size = old_node.get_content_length()
+            new_size = new_node.get_content_length()
+            return old_size + new_size
+
+        def _content_changes(old_node, new_node):
+            """Returns the list of differences.
+
+            The list is empty when no differences between comparable files
+            are detected, but the return value is None for non-comparable files.
+            """
+            old_content = old_node.get_content().read()
+            if is_binary(old_content):
+                return None
+
+            new_content = new_node.get_content().read()
+            if is_binary(new_content):
+                return None
+
+            mview = Mimeview(self.env)
+            old_content = mview.to_unicode(old_content, old_node.content_type)
+            new_content = mview.to_unicode(new_content, new_node.content_type)
+
+            if old_content != new_content:
+                context = 3
+                options = diff_options[1]
+                for option in options:
+                    if option.startswith('-U'):
+                        context = int(option[2:])
+                        break
+                if context < 0:
+                    context = None
+                tabwidth = self.config['diff'].getint('tab_width',
+                                self.config['mimeviewer'].getint('tab_width'))
+                return hdf_diff(old_content.splitlines(),
+                                new_content.splitlines(),
+                                context, tabwidth,
+                                ignore_blank_lines='-B' in options,
+                                ignore_case='-i' in options,
+                                ignore_space_changes='-b' in options)
+            else:
+                return []
+
+        if req.perm.has_permission('FILE_VIEW'):
+            diff_bytes = diff_files = 0
+            if self.max_diff_bytes or self.max_diff_files:
+                for old_node, new_node, kind, change in get_changes():
+                    if change == Changeset.EDIT and kind == Node.FILE:
+                        diff_files += 1
+                        diff_bytes += _estimate_changes(old_node, new_node)
+            show_diffs = (not self.max_diff_files or \
+                          diff_files <= self.max_diff_files) and \
+                         (not self.max_diff_bytes or \
+                          diff_bytes <= self.max_diff_bytes or \
+                          diff_files == 1)
+        else:
+            show_diffs = False
+
+        idx = 0
+        for old_node, new_node, kind, change in get_changes():
+            show_entry = change != Changeset.EDIT
+            if change in (Changeset.EDIT, Changeset.COPY, Changeset.MOVE) and \
+                   req.perm.has_permission('FILE_VIEW'):
+                assert old_node and new_node
+                props = _prop_changes(old_node, new_node)
+                if props:
+                    req.hdf['changeset.changes.%d.props' % idx] = props
+                    show_entry = True
+                if kind == Node.FILE and show_diffs:
+                    diffs = _content_changes(old_node, new_node)
+                    if diffs != []:
+                        if diffs:
+                            req.hdf['changeset.changes.%d.diff' % idx] = diffs
+                        # elif None (means: manually compare to (previous))
+                        show_entry = True
+            if show_entry or not show_diffs:
+                info = _change_info(old_node, new_node, change)
+                if change == Changeset.EDIT and not show_diffs:
+                    if chgset:
+                        diff_href = req.href.changeset(new_node.rev,
+                                                       new_node.path)
+                    else:
+                        diff_href = req.href.changeset(
+                            new_node.created_rev, new_node.created_path,
+                            old=old_node.created_rev,
+                            old_path=old_node.created_path)
+                    info['diff_href'] = diff_href
+                req.hdf['changeset.changes.%d' % idx] = info
+            idx += 1 # the sequence should be immutable
+
+    def _render_diff(self, req, filename, repos, diff, diff_options):
+        """Raw Unified Diff version"""
+        req.send_response(200)
+        req.send_header('Content-Type', 'text/plain;charset=utf-8')
+        req.send_header('Content-Disposition', 'inline;'
+                        'filename=%s.diff' % filename)
+        req.end_headers()
+
+        mimeview = Mimeview(self.env)
+        for old_node, new_node, kind, change in repos.get_changes(**diff):
+            # TODO: Property changes
+
+            # Content changes
+            if kind == Node.DIRECTORY:
+                continue
+
+            new_content = old_content = ''
+            new_node_info = old_node_info = ('','')
+            mimeview = Mimeview(self.env)
+
+            if old_node:
+                old_content = old_node.get_content().read()
+                if is_binary(old_content):
+                    continue
+                old_node_info = (old_node.path, old_node.rev)
+                old_content = mimeview.to_unicode(old_content,
+                                                  old_node.content_type)
+            if new_node:
+                new_content = new_node.get_content().read()
+                if is_binary(new_content):
+                    continue
+                new_node_info = (new_node.path, new_node.rev)
+                new_path = new_node.path
+                new_content = mimeview.to_unicode(new_content,
+                                                  new_node.content_type)
+            else:
+                old_node_path = repos.normalize_path(old_node.path)
+                diff_old_path = repos.normalize_path(diff.old_path)
+                new_path = posixpath.join(diff.new_path,
+                                          old_node_path[len(diff_old_path)+1:])
+
+            if old_content != new_content:
+                context = 3
+                options = diff_options[1]
+                for option in options:
+                    if option.startswith('-U'):
+                        context = int(option[2:])
+                        break
+                if not old_node_info[0]:
+                    old_node_info = new_node_info # support for 'A'dd changes
+                req.write('Index: ' + new_path + CRLF)
+                req.write('=' * 67 + CRLF)
+                req.write('--- %s (revision %s)' % old_node_info + CRLF)
+                req.write('+++ %s (revision %s)' % new_node_info + CRLF)
+                for line in unified_diff(old_content.splitlines(),
+                                         new_content.splitlines(), context,
+                                         ignore_blank_lines='-B' in options,
+                                         ignore_case='-i' in options,
+                                         ignore_space_changes='-b' in options):
+                    req.write(line + CRLF)
+
+    def _render_zip(self, req, filename, repos, diff):
+        """ZIP archive with all the added and/or modified files."""
+        new_rev = diff.new_rev
+        req.send_response(200)
+        req.send_header('Content-Type', 'application/zip')
+        req.send_header('Content-Disposition', 'attachment;'
+                        'filename=%s.zip' % filename)
+
+        from zipfile import ZipFile, ZipInfo, ZIP_DEFLATED
+
+        buf = StringIO()
+        zipfile = ZipFile(buf, 'w', ZIP_DEFLATED)
+        for old_node, new_node, kind, change in repos.get_changes(**diff):
+            if kind == Node.FILE and change != Changeset.DELETE:
+                assert new_node
+                zipinfo = ZipInfo()
+                zipinfo.filename = new_node.path.encode('utf-8')
+                # Note: unicode filenames are not supported by zipfile.
+                # UTF-8 is not supported by all Zip tools either,
+                # but as some does, I think UTF-8 is the best option here.
+                zipinfo.date_time = time.gmtime(new_node.last_modified)[:6]
+                zipinfo.compress_type = ZIP_DEFLATED
+                zipfile.writestr(zipinfo, new_node.get_content().read())
+        zipfile.close()
+
+        buf.seek(0, 2) # be sure to be at the end
+        req.send_header("Content-Length", buf.tell())
+        req.end_headers()
+
+        req.write(buf.getvalue())
+
+    def title_for_diff(self, diff):
+        if diff.new_path == diff.old_path: # ''diff between 2 revisions'' mode
+            return 'Diff r%s:%s for %s' \
+                   % (diff.old_rev or 'latest', diff.new_rev or 'latest',
+                      diff.new_path or '/')
+        else:                              # ''arbitrary diff'' mode
+            return 'Diff from %s@%s to %s@%s' \
+                   % (diff.old_path or '/', diff.old_rev or 'latest',
+                      diff.new_path or '/', diff.new_rev or 'latest')
+
+    # ITimelineEventProvider methods
+
+    def get_timeline_filters(self, req):
+        if req.perm.has_permission('CHANGESET_VIEW'):
+            yield ('changeset', 'Repository checkins')
+
+    def get_timeline_events(self, req, start, stop, filters):
+        if 'changeset' in filters:
+            format = req.args.get('format')
+            wiki_format = self.wiki_format_messages
+            show_files = self.timeline_show_files
+            db = self.env.get_db_cnx()
+            repos = self.env.get_repository(req.authname)
+            for chgset in repos.get_changesets(start, stop):
+                message = chgset.message or '--'
+                if wiki_format:
+                    shortlog = wiki_to_oneliner(message, self.env, db,
+                                                shorten=True)
+                else:
+                    shortlog = shorten_line(message)
+
+                if format == 'rss':
+                    title = Markup('Changeset [%s]: %s', chgset.rev, shortlog)
+                    href = req.abs_href.changeset(chgset.rev)
+                    if wiki_format:
+                        message = wiki_to_html(message, self.env, req, db,
+                                               absurls=True)
+                    else:
+                        message = html.PRE(message)
+                else:
+                    title = Markup('Changeset <em>[%s]</em> by %s', chgset.rev,
+                                   chgset.author)
+                    href = req.href.changeset(chgset.rev)
+
+                    if wiki_format:
+                        if self.timeline_long_messages:
+                            message = wiki_to_html(message, self.env, req, db,
+                                                   absurls=True)
+                        else:
+                            message = wiki_to_oneliner(message, self.env, db,
+                                                       shorten=True)
+                    else:
+                        message = shortlog
+
+                if show_files and req.perm.has_permission('BROWSER_VIEW'):
+                    files = []
+                    for chg in chgset.get_changes():
+                        if show_files > 0 and len(files) >= show_files:
+                            files.append(html.LI(Markup('&hellip;')))
+                            break
+                        files.append(html.LI(html.DIV(class_=chg[2]),
+                                             chg[0] or '/'))
+                    message = html.UL(files, class_="changes") + message
+
+                yield 'changeset', href, title, chgset.date, chgset.author,\
+                      message
+
+    # IWikiSyntaxProvider methods
+
+    CHANGESET_ID = r"(?:\d+|[a-fA-F\d]{6,})" # only "long enough" hexa ids
+
+    def get_wiki_syntax(self):
+        yield (
+            # [...] form: start with optional intertrac: [T... or [trac ...
+            r"!?\[(?P<it_changeset>%s\s*)" % Formatter.INTERTRAC_SCHEME +
+            # hex digits + optional /path for the restricted changeset
+            r"%s(?:/[^\]]*)?\]|" % self.CHANGESET_ID +
+            # r... form: allow r1 but not r1:2 (handled by the log syntax)
+            r"(?:\b|!)r%s\b(?!:%s)" % ((self.CHANGESET_ID,)*2),
+            lambda x, y, z:
+            self._format_changeset_link(x, 'changeset',
+                                        y[0] == 'r' and y[1:] or y[1:-1],
+                                        y, z))
+
+    def get_link_resolvers(self):
+        yield ('changeset', self._format_changeset_link)
+        yield ('diff', self._format_diff_link)
+
+    def _format_changeset_link(self, formatter, ns, chgset, label,
+                               fullmatch=None):
+        intertrac = formatter.shorthand_intertrac_helper(ns, chgset, label,
+                                                         fullmatch)
+        if intertrac:
+            return intertrac
+        sep = chgset.find('/')
+        if sep > 0:
+            rev, path = chgset[:sep], chgset[sep:]
+        else:
+            rev, path = chgset, None
+        cursor = formatter.db.cursor()
+        cursor.execute('SELECT message FROM revision WHERE rev=%s', (rev,))
+        row = cursor.fetchone()
+        if row:
+            return html.A(label, class_="changeset",
+                          title=shorten_line(row[0]),
+                          href=formatter.href.changeset(rev, path))
+        else:
+            return html.A(label, class_="missing changeset",
+                          href=formatter.href.changeset(rev, path),
+                          rel="nofollow")
+
+    def _format_diff_link(self, formatter, ns, params, label):
+        def pathrev(path):
+            if '@' in path:
+                return path.split('@', 1)
+            else:
+                return (path, None)
+        if '//' in params:
+            p1, p2 = params.split('//', 1)
+            old, new = pathrev(p1), pathrev(p2)
+            diff = DiffArgs(old_path=old[0], old_rev=old[1],
+                            new_path=new[0], new_rev=new[1])
+        else:
+            old_path, old_rev = pathrev(params)
+            new_rev = None
+            if old_rev and ':' in old_rev:
+                old_rev, new_rev = old_rev.split(':', 1)
+            diff = DiffArgs(old_path=old_path, old_rev=old_rev,
+                            new_path=old_path, new_rev=new_rev)
+        title = self.title_for_diff(diff)
+        href = formatter.href.changeset(new_path=diff.new_path or None,
+                                        new=diff.new_rev,
+                                        old_path=diff.old_path or None,
+                                        old=diff.old_rev)
+        return html.A(label, class_="changeset", title=title, href=href)
+
+    # ISearchSource methods
+
+    def get_search_filters(self, req):
+        if req.perm.has_permission('CHANGESET_VIEW'):
+            yield ('changeset', 'Changesets')
+
+    def get_search_results(self, req, terms, filters):
+        if not 'changeset' in filters:
+            return
+        authzperm = SubversionAuthorizer(self.env, req.authname)
+        db = self.env.get_db_cnx()
+        sql, args = search_to_sql(db, ['message', 'author'], terms)
+        cursor = db.cursor()
+        cursor.execute("SELECT rev,time,author,message "
+                       "FROM revision WHERE " + sql, args)
+        for rev, date, author, log in cursor:
+            if not authzperm.has_permission_for_changeset(rev):
+                continue
+            yield (req.href.changeset(rev),
+                   '[%s]: %s' % (rev, shorten_line(log)),
+                   date, author, shorten_result(log, terms))
+
+
+class AnyDiffModule(Component):
+
+    implements(IRequestHandler)
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        return re.match(r'/anydiff$', req.path_info)
+
+    def process_request(self, req):
+        # -- retrieve arguments
+        new_path = req.args.get('new_path')
+        new_rev = req.args.get('new_rev')
+        old_path = req.args.get('old_path')
+        old_rev = req.args.get('old_rev')
+
+        # -- normalize
+        repos = self.env.get_repository(req.authname)
+        new_path = repos.normalize_path(new_path)
+        new_rev = repos.normalize_rev(new_rev)
+        old_path = repos.normalize_path(old_path)
+        old_rev = repos.normalize_rev(old_rev)
+
+        authzperm = SubversionAuthorizer(self.env, req.authname)
+        authzperm.assert_permission_for_changeset(new_rev)
+        authzperm.assert_permission_for_changeset(old_rev)
+
+        # -- prepare rendering
+        req.hdf['anydiff'] = {
+            'new_path': new_path,
+            'new_rev': new_rev,
+            'old_path': old_path,
+            'old_rev': old_rev,
+            'changeset_href': req.href.changeset(),
+        }
+
+        return 'anydiff.cs', None
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/web_ui/log.py
@@ -0,0 +1,249 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christian Boos <cboos@neuf.fr>
+
+import re
+import urllib
+
+from trac.core import *
+from trac.perm import IPermissionRequestor
+from trac.util.datefmt import http_date
+from trac.util.markup import html
+from trac.versioncontrol import Changeset
+from trac.versioncontrol.web_ui.changeset import ChangesetModule
+from trac.versioncontrol.web_ui.util import *
+from trac.web import IRequestHandler
+from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
+from trac.wiki import IWikiSyntaxProvider, Formatter
+
+LOG_LIMIT = 100
+
+class LogModule(Component):
+
+    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
+               IWikiSyntaxProvider)
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'browser'
+
+    def get_navigation_items(self, req):
+        return []
+
+    # IPermissionRequestor methods
+
+    def get_permission_actions(self):
+        return ['LOG_VIEW']
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        import re
+        match = re.match(r'/log(?:(/.*)|$)', req.path_info)
+        if match:
+            req.args['path'] = match.group(1) or '/'
+            return True
+
+    def process_request(self, req):
+        req.perm.assert_permission('LOG_VIEW')
+
+        mode = req.args.get('mode', 'stop_on_copy')
+        path = req.args.get('path', '/')
+        rev = req.args.get('rev')
+        stop_rev = req.args.get('stop_rev')
+        format = req.args.get('format')
+        verbose = req.args.get('verbose')
+        limit = LOG_LIMIT
+
+        repos = self.env.get_repository(req.authname)
+        normpath = repos.normalize_path(path)
+        rev = unicode(repos.normalize_rev(rev))
+        if stop_rev:
+            stop_rev = unicode(repos.normalize_rev(stop_rev))
+            if repos.rev_older_than(rev, stop_rev):
+                rev, stop_rev = stop_rev, rev
+            
+        req.hdf['title'] = path + ' (log)'
+        req.hdf['log'] = {
+            'mode': mode,
+            'path': path,
+            'rev': rev,
+            'verbose': verbose,
+            'stop_rev': stop_rev,
+            'browser_href': req.href.browser(path),
+            'changeset_href': req.href.changeset(),
+            'log_href': req.href.log(path, rev=rev)
+        }
+
+        path_links = get_path_links(req.href, path, rev)
+        req.hdf['log.path'] = path_links
+        if path_links:
+            add_link(req, 'up', path_links[-1]['href'], 'Parent directory')
+
+        # The `history()` method depends on the mode:
+        #  * for ''stop on copy'' and ''follow copies'', it's `Node.history()` 
+        #  * for ''show only add, delete'' it's`Repository.get_path_history()` 
+        if mode == 'path_history':
+            def history(limit):
+                for h in repos.get_path_history(path, rev, limit):
+                    yield h
+        else:
+            history = get_existing_node(req, repos, path, rev).get_history
+
+        # -- retrieve history, asking for limit+1 results
+        info = []
+        previous_path = repos.normalize_path(path)
+        for old_path, old_rev, old_chg in history(limit+1):
+            if stop_rev and repos.rev_older_than(old_rev, stop_rev):
+                break
+            old_path = repos.normalize_path(old_path)
+            item = {
+                'rev': str(old_rev),
+                'path': old_path,
+                'log_href': req.href.log(old_path, rev=old_rev),
+                'browser_href': req.href.browser(old_path, rev=old_rev),
+                'changeset_href': req.href.changeset(old_rev),
+                'restricted_href': req.href.changeset(old_rev, new_path=old_path),
+                'change': old_chg
+            }
+            if not (mode == 'path_history' and old_chg == Changeset.EDIT):
+                info.append(item)
+            if old_path and old_path != previous_path \
+               and not (mode == 'path_history' and old_path == normpath):
+                item['copyfrom_path'] = old_path
+                if mode == 'stop_on_copy':
+                    break
+            if len(info) > limit: # we want limit+1 entries
+                break
+            previous_path = old_path
+        if info == []:
+            # FIXME: we should send a 404 error here
+            raise TracError("The file or directory '%s' doesn't exist "
+                            "at revision %s or at any previous revision."
+                            % (path, rev), 'Nonexistent path')
+
+        def make_log_href(path, **args):
+            link_rev = rev
+            if rev == str(repos.youngest_rev):
+                link_rev = None
+            params = {'rev': link_rev, 'mode': mode, 'limit': limit}
+            params.update(args)
+            if verbose:
+                params['verbose'] = verbose
+            return req.href.log(path, **params)
+
+        if len(info) == limit+1: # limit+1 reached, there _might_ be some more
+            next_rev = info[-1]['rev']
+            next_path = info[-1]['path']
+            add_link(req, 'next', make_log_href(next_path, rev=next_rev),
+                     'Revision Log (restarting at %s, rev. %s)'
+                     % (next_path, next_rev))
+            # now, only show 'limit' results
+            del info[-1]
+        
+        req.hdf['log.items'] = info
+
+        revs = [i['rev'] for i in info]
+        changes = get_changes(self.env, repos, revs, verbose, req, format)
+        if format == 'rss':
+            # Get the email addresses of all known users
+            email_map = {}
+            for username,name,email in self.env.get_known_users():
+                if email:
+                    email_map[username] = email
+            for cs in changes.values():
+                # For RSS, author must be an email address
+                author = cs['author']
+                author_email = ''
+                if '@' in author:
+                    author_email = author
+                elif email_map.has_key(author):
+                    author_email = email_map[author]
+                cs['author'] = author_email
+                cs['date'] = http_date(cs['date_seconds'])
+        elif format == 'changelog':
+            for rev in revs:
+                changeset = repos.get_changeset(rev)
+                cs = changes[rev]
+                cs['message'] = '\n'.join(['\t' + m for m in
+                                           changeset.message.split('\n')])
+                files = []
+                actions = []
+                for path, kind, chg, bpath, brev in changeset.get_changes():
+                    files.append(chg == Changeset.DELETE and bpath or path)
+                    actions.append(chg)
+                cs['files'] = files
+                cs['actions'] = actions
+        req.hdf['log.changes'] = changes
+
+        if req.args.get('format') == 'changelog':
+            return 'log_changelog.cs', 'text/plain'
+        elif req.args.get('format') == 'rss':
+            return 'log_rss.cs', 'application/rss+xml'
+
+        add_stylesheet(req, 'common/css/browser.css')
+        add_stylesheet(req, 'common/css/diff.css')
+
+        rss_href = make_log_href(path, format='rss', stop_rev=stop_rev)
+        add_link(req, 'alternate', rss_href, 'RSS Feed', 'application/rss+xml',
+                 'rss')
+        changelog_href = make_log_href(path, format='changelog',
+                                       stop_rev=stop_rev)
+        add_link(req, 'alternate', changelog_href, 'ChangeLog', 'text/plain')
+
+        return 'log.cs', None
+
+    # IWikiSyntaxProvider methods
+
+    REV_RANGE = "%s[-:]%s" % ((ChangesetModule.CHANGESET_ID,)*2)
+    
+    def get_wiki_syntax(self):
+        yield (
+            # [...] form, starts with optional intertrac: [T... or [trac ...
+            r"!?\[(?P<it_log>%s\s*)" % Formatter.INTERTRAC_SCHEME +
+            # <from>:<to> + optional path restriction
+            r"(?P<log_rev>%s)(?P<log_path>/[^\]]*)?\]" % self.REV_RANGE,
+            lambda x, y, z: self._format_link(x, 'log1', y[1:-1], y, z))
+        yield (
+            # r<from>:<to> form (no intertrac and no path restriction)
+            r"(?:\b|!)r%s\b" % self.REV_RANGE,
+            lambda x, y, z: self._format_link(x, 'log2', '@' + y[1:], y))
+
+    def get_link_resolvers(self):
+        yield ('log', self._format_link)
+
+    def _format_link(self, formatter, ns, match, label, fullmatch=None):
+        if ns == 'log1':
+            it_log = fullmatch.group('it_log')
+            rev = fullmatch.group('log_rev')
+            path = fullmatch.group('log_path')
+        else: # ns == 'log2'
+            path, rev, line = get_path_rev_line(match)
+        stop_rev = None
+        for sep in ':-':
+            if not stop_rev and rev and sep in rev:
+                stop_rev, rev = rev.split(sep, 1)
+        href = formatter.href.log(path or '/', rev=rev, stop_rev=stop_rev)
+        if ns == 'log1':
+            target = it_log + href[len(formatter.href.log('/')):]
+            # prepending it_log is needed, as the helper expects it there
+            intertrac = formatter.shorthand_intertrac_helper('log', target,
+                                                             label, fullmatch)
+            if intertrac:
+                return intertrac
+        return html.A(label, href=href, class_='source')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/web_ui/tests/__init__.py
@@ -0,0 +1,11 @@
+import unittest
+
+from trac.versioncontrol.web_ui.tests import wikisyntax
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(wikisyntax.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/web_ui/tests/wikisyntax.py
@@ -0,0 +1,219 @@
+import unittest
+
+from trac.test import Mock
+from trac.wiki.tests import formatter
+from trac.versioncontrol.web_ui import *
+
+CHANGESET_TEST_CASES="""
+============================== changeset: link resolver
+changeset:1
+changeset:12
+changeset:abc
+changeset:1, changeset:1/README.txt
+------------------------------
+<p>
+<a class="missing changeset" href="/changeset/1" rel="nofollow">changeset:1</a>
+<a class="missing changeset" href="/changeset/12" rel="nofollow">changeset:12</a>
+<a class="missing changeset" href="/changeset/abc" rel="nofollow">changeset:abc</a>
+<a class="missing changeset" href="/changeset/1" rel="nofollow">changeset:1</a>, <a class="missing changeset" href="/changeset/1/README.txt" rel="nofollow">changeset:1/README.txt</a>
+</p>
+------------------------------
+============================== changeset shorthand syntax
+[1], r1
+[12], r12, rABC
+[1/README.txt]
+------------------------------
+<p>
+<a class="missing changeset" href="/changeset/1" rel="nofollow">[1]</a>, <a class="missing changeset" href="/changeset/1" rel="nofollow">r1</a>
+<a class="missing changeset" href="/changeset/12" rel="nofollow">[12]</a>, <a class="missing changeset" href="/changeset/12" rel="nofollow">r12</a>, rABC
+<a class="missing changeset" href="/changeset/1/README.txt" rel="nofollow">[1/README.txt]</a>
+</p>
+------------------------------
+============================== escaping the above
+![1], !r1
+------------------------------
+<p>
+[1], r1
+</p>
+------------------------------
+[1], r1
+============================== Link resolver counter examples
+Change:[10] There should be a link to changeset [10]
+
+rfc and rfc:4180 should not be changeset links
+------------------------------
+<p>
+Change:<a class="missing changeset" href="/changeset/10" rel="nofollow">[10]</a> There should be a link to changeset <a class="missing changeset" href="/changeset/10" rel="nofollow">[10]</a>
+</p>
+<p>
+rfc and rfc:4180 should not be changeset links
+</p>
+------------------------------
+Change:<a class="missing changeset" href="/changeset/10" rel="nofollow">[10]</a> There should be a link to changeset <a class="missing changeset" href="/changeset/10" rel="nofollow">[10]</a>
+
+rfc and rfc:4180 should not be changeset links
+============================== InterTrac for changesets
+trac:changeset:2081
+[trac:changeset:2081 Trac r2081]
+------------------------------
+<p>
+<a class="ext-link" href="http://projects.edgewall.com/trac/changeset/2081" title="changeset:2081 in Trac's Trac"><span class="icon">trac:changeset:2081</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/changeset/2081" title="changeset:2081 in Trac's Trac"><span class="icon">Trac r2081</span></a>
+</p>
+------------------------------
+============================== Changeset InterTrac shorthands
+[T2081]
+[trac 2081]
+[trac 2081/trunk]
+T:r2081
+------------------------------
+<p>
+<a class="ext-link" href="http://projects.edgewall.com/trac/changeset/2081" title="changeset:2081 in Trac's Trac"><span class="icon">[T2081]</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/changeset/2081" title="changeset:2081 in Trac's Trac"><span class="icon">[trac 2081]</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/changeset/2081/trunk" title="changeset:2081/trunk in Trac\'s Trac"><span class="icon">[trac 2081/trunk]</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=r2081" title="r2081 in Trac's Trac"><span class="icon">T:r2081</span></a>
+</p>
+------------------------------
+""" #'
+
+def _get_changeset(self, x):
+    raise TracError("No changeset")
+
+def _get_repository(self):
+    return Mock(get_changeset=_get_changeset)
+
+def changeset_setup(tc):
+    setattr(tc.env, 'get_repository', _get_repository)
+
+
+LOG_TEST_CASES="""
+============================== Log range TracLinks
+[1:2], r1:2, [12:23], r12:23
+[1:2/trunk]
+------------------------------
+<p>
+<a class="source" href="/log/?rev=2&amp;stop_rev=1">[1:2]</a>, <a class="source" href="/log/?rev=2&amp;stop_rev=1">r1:2</a>, <a class="source" href="/log/?rev=23&amp;stop_rev=12">[12:23]</a>, <a class="source" href="/log/?rev=23&amp;stop_rev=12">r12:23</a>
+<a class="source" href="/log/trunk?rev=2&amp;stop_rev=1">[1:2/trunk]</a>
+</p>
+------------------------------
+============================== Escaping Log range TracLinks
+![1:2], !r1:2, ![12:23], !r12:23
+------------------------------
+<p>
+[1:2], r1:2, [12:23], r12:23
+</p>
+------------------------------
+[1:2], r1:2, [12:23], r12:23
+============================== log: link resolver
+log:@12
+log:trunk
+log:trunk@12
+log:trunk@12:23
+log:trunk@12-23
+log:trunk:12:23
+log:trunk:12-23
+------------------------------
+<p>
+<a class="source" href="/log/?rev=12">log:@12</a>
+<a class="source" href="/log/trunk">log:trunk</a>
+<a class="source" href="/log/trunk?rev=12">log:trunk@12</a>
+<a class="source" href="/log/trunk?rev=23&amp;stop_rev=12">log:trunk@12:23</a>
+<a class="source" href="/log/trunk?rev=23&amp;stop_rev=12">log:trunk@12-23</a>
+<a class="source" href="/log/trunk?rev=23&amp;stop_rev=12">log:trunk:12:23</a>
+<a class="source" href="/log/trunk?rev=23&amp;stop_rev=12">log:trunk:12-23</a>
+</p>
+------------------------------
+============================== Link resolver counter examples
+rfc:4180 should not be a log link
+------------------------------
+<p>
+rfc:4180 should not be a log link
+</p>
+------------------------------
+============================== Log range InterTrac shorthands
+[T3317:3318]
+[trac 3317:3318]
+[trac 3317:3318/trunk]
+------------------------------
+<p>
+<a class="ext-link" href="http://projects.edgewall.com/trac/log/?rev=3318&amp;stop_rev=3317" title="log:?rev=3318&amp;stop_rev=3317 in Trac\'s Trac"><span class="icon">[T3317:3318]</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/log/?rev=3318&amp;stop_rev=3317" title="log:?rev=3318&amp;stop_rev=3317 in Trac\'s Trac"><span class="icon">[trac 3317:3318]</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/log/trunk?rev=3318&amp;stop_rev=3317" title="log:trunk?rev=3318&amp;stop_rev=3317 in Trac\'s Trac"><span class="icon">[trac 3317:3318/trunk]</span></a>
+</p>
+------------------------------
+"""
+
+
+DIFF_TEST_CASES="""
+============================== diff: link resolver
+diff:trunk//branch
+diff:trunk@12//branch@23
+diff:trunk@12:23
+diff:@12:23
+------------------------------
+<p>
+<a class="changeset" href="/changeset?new_path=branch&amp;old_path=trunk" title="Diff from trunk@latest to branch@latest">diff:trunk//branch</a>
+<a class="changeset" href="/changeset?new=23&amp;new_path=branch&amp;old=12&amp;old_path=trunk" title="Diff from trunk@12 to branch@23">diff:trunk@12//branch@23</a>
+<a class="changeset" href="/changeset?new=23&amp;new_path=trunk&amp;old=12&amp;old_path=trunk" title="Diff r12:23 for trunk">diff:trunk@12:23</a>
+<a class="changeset" href="/changeset?new=23&amp;old=12" title="Diff r12:23 for /">diff:@12:23</a>
+</p>
+------------------------------
+"""
+
+
+SOURCE_TEST_CASES="""
+============================== source: link resolver
+source:/foo/bar
+source:/foo/bar#42
+source:/foo/bar#head
+source:/foo/bar@42
+source:/foo/bar@head
+source:/foo%20bar/baz%2Bquux
+source:/foo%2520bar/baz%252Bquux#42
+source:#42
+source:@42
+source:/foo/bar@42#L20
+source:/foo/bar@head#L20
+------------------------------
+<p>
+<a class="source" href="/browser/foo/bar">source:/foo/bar</a>
+<a class="source" href="/browser/foo/bar?rev=42">source:/foo/bar#42</a>
+<a class="source" href="/browser/foo/bar?rev=head">source:/foo/bar#head</a>
+<a class="source" href="/browser/foo/bar?rev=42">source:/foo/bar@42</a>
+<a class="source" href="/browser/foo/bar?rev=head">source:/foo/bar@head</a>
+<a class="source" href="/browser/foo%20bar/baz%2Bquux">source:/foo%20bar/baz%2Bquux</a>
+<a class="source" href="/browser/foo%2520bar/baz%252Bquux?rev=42">source:/foo%2520bar/baz%252Bquux#42</a>
+<a class="source" href="/browser/?rev=42">source:#42</a>
+<a class="source" href="/browser/?rev=42">source:@42</a>
+<a class="source" href="/browser/foo/bar?rev=42#L20">source:/foo/bar@42#L20</a>
+<a class="source" href="/browser/foo/bar?rev=head#L20">source:/foo/bar@head#L20</a>
+</p>
+------------------------------
+============================== source: provider, with quoting
+source:'even with whitespaces'
+source:"even with whitespaces"
+[source:'even with whitespaces' Path with spaces]
+[source:"even with whitespaces" Path with spaces]
+------------------------------
+<p>
+<a class="source" href="/browser/even%20with%20whitespaces">source:'even with whitespaces'</a>
+<a class="source" href="/browser/even%20with%20whitespaces">source:"even with whitespaces"</a>
+<a class="source" href="/browser/even%20with%20whitespaces">Path with spaces</a>
+<a class="source" href="/browser/even%20with%20whitespaces">Path with spaces</a>
+</p>
+------------------------------
+""" # " (be Emacs friendly...)
+
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(formatter.suite(CHANGESET_TEST_CASES, changeset_setup,
+                                  __file__))
+    suite.addTest(formatter.suite(LOG_TEST_CASES, file=__file__))
+    suite.addTest(formatter.suite(DIFF_TEST_CASES, file=__file__))
+    suite.addTest(formatter.suite(SOURCE_TEST_CASES, file=__file__))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/versioncontrol/web_ui/util.py
@@ -0,0 +1,120 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christian Boos <cboos@neuf.fr>
+
+import re
+import urllib
+
+from trac.core import TracError
+from trac.util.datefmt import format_datetime, pretty_timedelta
+from trac.util.text import shorten_line
+from trac.util.markup import escape, html, Markup
+from trac.versioncontrol.api import NoSuchNode, NoSuchChangeset
+from trac.wiki import wiki_to_html, wiki_to_oneliner
+
+__all__ = ['get_changes', 'get_path_links', 'get_path_rev_line',
+           'get_existing_node', 'render_node_property']
+
+def get_changes(env, repos, revs, full=None, req=None, format=None):
+    db = env.get_db_cnx()
+    changes = {}
+    for rev in revs:
+        try:
+            changeset = repos.get_changeset(rev)
+        except NoSuchChangeset:
+            changes[rev] = {}
+            continue
+
+        wiki_format = env.config['changeset'].getbool('wiki_format_messages')
+        message = changeset.message or '--'
+        absurls = (format == 'rss')
+        if wiki_format:
+            shortlog = wiki_to_oneliner(message, env, db,
+                                        shorten=True, absurls=absurls)
+        else:
+            shortlog = Markup.escape(shorten_line(message))
+
+        if full:
+            if wiki_format:
+                message = wiki_to_html(message, env, req, db,
+                                       absurls=absurls, escape_newlines=True)
+            else:
+                message = html.PRE(message)
+        else:
+            message = shortlog
+
+        if format == 'rss':
+            if isinstance(shortlog, Markup):
+                shortlog = shortlog.plaintext(keeplinebreaks=False)
+            message = unicode(message)
+
+        changes[rev] = {
+            'date_seconds': changeset.date,
+            'date': format_datetime(changeset.date),
+            'age': pretty_timedelta(changeset.date),
+            'author': changeset.author or 'anonymous',
+            'message': message, 'shortlog': shortlog,
+        }
+    return changes
+
+def get_path_links(href, path, rev):
+    links = []
+    parts = path.split('/')
+    if not parts[-1]:
+        parts.pop()
+    path = '/'
+    for part in parts:
+        path = path + part + '/'
+        links.append({
+            'name': part or 'root',
+            'href': href.browser(path, rev=rev)
+        })
+    return links
+
+rev_re = re.compile(r"([^@#:]*)[@#:]([^#]+)(?:#L(\d+))?")
+
+def get_path_rev_line(path):
+    rev = None
+    line = None
+    match = rev_re.search(path)
+    if match:
+        path = match.group(1)
+        rev = match.group(2)
+        if match.group(3):
+            line = int(match.group(3))
+    path = urllib.unquote(path)
+    return path, rev, line
+
+def get_existing_node(req, repos, path, rev):
+    try: 
+        return repos.get_node(path, rev) 
+    except NoSuchNode, e:
+        raise TracError(Markup('%s<br><p>You can <a href="%s">search</a> ' 
+                               'in the repository history to see if that path '
+                               'existed but was later removed.</p>', e.message,
+                               req.href.log(path, rev=rev,
+                                            mode='path_history')))
+
+def render_node_property(env, name, value):
+    """Renders a node property value to HTML.
+
+    Currently only handle multi-line properties. See also #1601.
+    """
+    if value and '\n' in value:
+        value = Markup(''.join(['<br />%s' % escape(v)
+                                for v in value.split('\n')]))
+    return value
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/__init__.py
@@ -0,0 +1,1 @@
+from trac.web.api import *
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/_fcgi.py
@@ -0,0 +1,1306 @@
+# -*- coding: iso-8859-1 -*-
+#
+# Copyright (c) 2002, 2003, 2005 Allan Saddi <allan@saddi.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Allan Saddi <allan@saddi.com>
+
+"""
+fcgi - a FastCGI/WSGI gateway.
+
+For more information about FastCGI, see <http://www.fastcgi.com/>.
+
+For more information about the Web Server Gateway Interface, see
+<http://www.python.org/peps/pep-0333.html>.
+
+Example usage:
+
+  #!/usr/bin/env python
+  from myapplication import app # Assume app is your WSGI application object
+  from fcgi import WSGIServer
+  WSGIServer(app).run()
+
+See the documentation for WSGIServer/Server for more information.
+
+On most platforms, fcgi will fallback to regular CGI behavior if run in a
+non-FastCGI context. If you want to force CGI behavior, set the environment
+variable FCGI_FORCE_CGI to "Y" or "y".
+"""
+
+__author__ = 'Allan Saddi <allan@saddi.com>'
+__version__ = '$Revision: 1797 $'
+
+import sys
+import os
+import signal
+import struct
+import StringIO
+import select
+import socket
+import errno
+import traceback
+
+try:
+    import thread
+    import threading
+    thread_available = True
+except ImportError:
+    import dummy_thread as thread
+    import dummy_threading as threading
+    thread_available = False
+
+__all__ = ['WSGIServer']
+
+# Constants from the spec.
+FCGI_LISTENSOCK_FILENO = 0
+
+FCGI_HEADER_LEN = 8
+
+FCGI_VERSION_1 = 1
+
+FCGI_BEGIN_REQUEST = 1
+FCGI_ABORT_REQUEST = 2
+FCGI_END_REQUEST = 3
+FCGI_PARAMS = 4
+FCGI_STDIN = 5
+FCGI_STDOUT = 6
+FCGI_STDERR = 7
+FCGI_DATA = 8
+FCGI_GET_VALUES = 9
+FCGI_GET_VALUES_RESULT = 10
+FCGI_UNKNOWN_TYPE = 11
+FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE
+
+FCGI_NULL_REQUEST_ID = 0
+
+FCGI_KEEP_CONN = 1
+
+FCGI_RESPONDER = 1
+FCGI_AUTHORIZER = 2
+FCGI_FILTER = 3
+
+FCGI_REQUEST_COMPLETE = 0
+FCGI_CANT_MPX_CONN = 1
+FCGI_OVERLOADED = 2
+FCGI_UNKNOWN_ROLE = 3
+
+FCGI_MAX_CONNS = 'FCGI_MAX_CONNS'
+FCGI_MAX_REQS = 'FCGI_MAX_REQS'
+FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS'
+
+FCGI_Header = '!BBHHBx'
+FCGI_BeginRequestBody = '!HB5x'
+FCGI_EndRequestBody = '!LB3x'
+FCGI_UnknownTypeBody = '!B7x'
+
+FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody)
+FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody)
+
+if __debug__:
+    import time
+
+    # Set non-zero to write debug output to a file.
+    DEBUG = 0
+    DEBUGLOG = '/tmp/fcgi.log'
+
+    def _debug(level, msg):
+        if DEBUG < level:
+            return
+
+        try:
+            f = open(DEBUGLOG, 'a')
+            f.write('%sfcgi: %s\n' % (time.ctime()[4:-4], msg))
+            f.close()
+        except:
+            pass
+
+class InputStream(object):
+    """
+    File-like object representing FastCGI input streams (FCGI_STDIN and
+    FCGI_DATA). Supports the minimum methods required by WSGI spec.
+    """
+    def __init__(self, conn):
+        self._conn = conn
+
+        # See Server.
+        self._shrinkThreshold = conn.server.inputStreamShrinkThreshold
+
+        self._buf = ''
+        self._bufList = []
+        self._pos = 0 # Current read position.
+        self._avail = 0 # Number of bytes currently available.
+
+        self._eof = False # True when server has sent EOF notification.
+
+    def _shrinkBuffer(self):
+        """Gets rid of already read data (since we can't rewind)."""
+        if self._pos >= self._shrinkThreshold:
+            self._buf = self._buf[self._pos:]
+            self._avail -= self._pos
+            self._pos = 0
+
+            assert self._avail >= 0
+
+    def _waitForData(self):
+        """Waits for more data to become available."""
+        self._conn.process_input()
+
+    def read(self, n=-1):
+        if self._pos == self._avail and self._eof:
+            return ''
+        while True:
+            if n < 0 or (self._avail - self._pos) < n:
+                # Not enough data available.
+                if self._eof:
+                    # And there's no more coming.
+                    newPos = self._avail
+                    break
+                else:
+                    # Wait for more data.
+                    self._waitForData()
+                    continue
+            else:
+                newPos = self._pos + n
+                break
+        # Merge buffer list, if necessary.
+        if self._bufList:
+            self._buf += ''.join(self._bufList)
+            self._bufList = []
+        r = self._buf[self._pos:newPos]
+        self._pos = newPos
+        self._shrinkBuffer()
+        return r
+
+    def readline(self, length=None):
+        if self._pos == self._avail and self._eof:
+            return ''
+        while True:
+            # Unfortunately, we need to merge the buffer list early.
+            if self._bufList:
+                self._buf += ''.join(self._bufList)
+                self._bufList = []
+            # Find newline.
+            i = self._buf.find('\n', self._pos)
+            if i < 0:
+                # Not found?
+                if self._eof:
+                    # No more data coming.
+                    newPos = self._avail
+                    break
+                else:
+                    # Wait for more to come.
+                    self._waitForData()
+                    continue
+            else:
+                newPos = i + 1
+                break
+        if length is not None:
+            if self._pos + length < newPos:
+                newPos = self._pos + length
+        r = self._buf[self._pos:newPos]
+        self._pos = newPos
+        self._shrinkBuffer()
+        return r
+
+    def readlines(self, sizehint=0):
+        total = 0
+        lines = []
+        line = self.readline()
+        while line:
+            lines.append(line)
+            total += len(line)
+            if 0 < sizehint <= total:
+                break
+            line = self.readline()
+        return lines
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        r = self.readline()
+        if not r:
+            raise StopIteration
+        return r
+
+    def add_data(self, data):
+        if not data:
+            self._eof = True
+        else:
+            self._bufList.append(data)
+            self._avail += len(data)
+
+class MultiplexedInputStream(InputStream):
+    """
+    A version of InputStream meant to be used with MultiplexedConnections.
+    Assumes the MultiplexedConnection (the producer) and the Request
+    (the consumer) are running in different threads.
+    """
+    def __init__(self, conn):
+        super(MultiplexedInputStream, self).__init__(conn)
+
+        # Arbitrates access to this InputStream (it's used simultaneously
+        # by a Request and its owning Connection object).
+        lock = threading.RLock()
+
+        # Notifies Request thread that there is new data available.
+        self._lock = threading.Condition(lock)
+
+    def _waitForData(self):
+        # Wait for notification from add_data().
+        self._lock.wait()
+
+    def read(self, n=-1):
+        self._lock.acquire()
+        try:
+            return super(MultiplexedInputStream, self).read(n)
+        finally:
+            self._lock.release()
+
+    def readline(self, length=None):
+        self._lock.acquire()
+        try:
+            return super(MultiplexedInputStream, self).readline(length)
+        finally:
+            self._lock.release()
+
+    def add_data(self, data):
+        self._lock.acquire()
+        try:
+            super(MultiplexedInputStream, self).add_data(data)
+            self._lock.notify()
+        finally:
+            self._lock.release()
+
+class OutputStream(object):
+    """
+    FastCGI output stream (FCGI_STDOUT/FCGI_STDERR). By default, calls to
+    write() or writelines() immediately result in Records being sent back
+    to the server. Buffering should be done in a higher level!
+    """
+    def __init__(self, conn, req, type, buffered=False):
+        self._conn = conn
+        self._req = req
+        self._type = type
+        self._buffered = buffered
+        self._bufList = [] # Used if buffered is True
+        self.dataWritten = False
+        self.closed = False
+
+    def _write(self, data):
+        length = len(data)
+        while length:
+            toWrite = min(length, self._req.server.maxwrite - FCGI_HEADER_LEN)
+
+            rec = Record(self._type, self._req.requestId)
+            rec.contentLength = toWrite
+            rec.contentData = data[:toWrite]
+            self._conn.writeRecord(rec)
+
+            data = data[toWrite:]
+            length -= toWrite
+
+    def write(self, data):
+        assert not self.closed
+
+        if not data:
+            return
+
+        self.dataWritten = True
+
+        if self._buffered:
+            self._bufList.append(data)
+        else:
+            self._write(data)
+
+    def writelines(self, lines):
+        assert not self.closed
+
+        for line in lines:
+            self.write(line)
+
+    def flush(self):
+        # Only need to flush if this OutputStream is actually buffered.
+        if self._buffered:
+            data = ''.join(self._bufList)
+            self._bufList = []
+            self._write(data)
+
+    # Though available, the following should NOT be called by WSGI apps.
+    def close(self):
+        """Sends end-of-stream notification, if necessary."""
+        if not self.closed and self.dataWritten:
+            self.flush()
+            rec = Record(self._type, self._req.requestId)
+            self._conn.writeRecord(rec)
+            self.closed = True
+
+class TeeOutputStream(object):
+    """
+    Simple wrapper around two or more output file-like objects that copies
+    written data to all streams.
+    """
+    def __init__(self, streamList):
+        self._streamList = streamList
+
+    def write(self, data):
+        for f in self._streamList:
+            f.write(data)
+
+    def writelines(self, lines):
+        for line in lines:
+            self.write(line)
+
+    def flush(self):
+        for f in self._streamList:
+            f.flush()
+
+class StdoutWrapper(object):
+    """
+    Wrapper for sys.stdout so we know if data has actually been written.
+    """
+    def __init__(self, stdout):
+        self._file = stdout
+        self.dataWritten = False
+
+    def write(self, data):
+        if data:
+            self.dataWritten = True
+        self._file.write(data)
+
+    def writelines(self, lines):
+        for line in lines:
+            self.write(line)
+
+    def __getattr__(self, name):
+        return getattr(self._file, name)
+
+def decode_pair(s, pos=0):
+    """
+    Decodes a name/value pair.
+
+    The number of bytes decoded as well as the name/value pair
+    are returned.
+    """
+    nameLength = ord(s[pos])
+    if nameLength & 128:
+        nameLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff
+        pos += 4
+    else:
+        pos += 1
+
+    valueLength = ord(s[pos])
+    if valueLength & 128:
+        valueLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff
+        pos += 4
+    else:
+        pos += 1
+
+    name = s[pos:pos+nameLength]
+    pos += nameLength
+    value = s[pos:pos+valueLength]
+    pos += valueLength
+
+    return (pos, (name, value))
+
+def encode_pair(name, value):
+    """
+    Encodes a name/value pair.
+
+    The encoded string is returned.
+    """
+    nameLength = len(name)
+    if nameLength < 128:
+        s = chr(nameLength)
+    else:
+        s = struct.pack('!L', nameLength | 0x80000000L)
+
+    valueLength = len(value)
+    if valueLength < 128:
+        s += chr(valueLength)
+    else:
+        s += struct.pack('!L', valueLength | 0x80000000L)
+
+    return s + name + value
+    
+class Record(object):
+    """
+    A FastCGI Record.
+
+    Used for encoding/decoding records.
+    """
+    def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID):
+        self.version = FCGI_VERSION_1
+        self.type = type
+        self.requestId = requestId
+        self.contentLength = 0
+        self.paddingLength = 0
+        self.contentData = ''
+
+    def _recvall(sock, length):
+        """
+        Attempts to receive length bytes from a socket, blocking if necessary.
+        (Socket may be blocking or non-blocking.)
+        """
+        dataList = []
+        recvLen = 0
+        while length:
+            try:
+                data = sock.recv(length)
+            except socket.error, e:
+                if e[0] == errno.EAGAIN:
+                    select.select([sock], [], [])
+                    continue
+                else:
+                    raise
+            if not data: # EOF
+                break
+            dataList.append(data)
+            dataLen = len(data)
+            recvLen += dataLen
+            length -= dataLen
+        return ''.join(dataList), recvLen
+    _recvall = staticmethod(_recvall)
+
+    def read(self, sock):
+        """Read and decode a Record from a socket."""
+        try:
+            header, length = self._recvall(sock, FCGI_HEADER_LEN)
+        except:
+            raise EOFError
+
+        if length < FCGI_HEADER_LEN:
+            raise EOFError
+        
+        self.version, self.type, self.requestId, self.contentLength, \
+                      self.paddingLength = struct.unpack(FCGI_Header, header)
+
+        if __debug__: _debug(9, 'read: fd = %d, type = %d, requestId = %d, '
+                             'contentLength = %d' %
+                             (sock.fileno(), self.type, self.requestId,
+                              self.contentLength))
+        
+        if self.contentLength:
+            try:
+                self.contentData, length = self._recvall(sock,
+                                                         self.contentLength)
+            except:
+                raise EOFError
+
+            if length < self.contentLength:
+                raise EOFError
+
+        if self.paddingLength:
+            try:
+                self._recvall(sock, self.paddingLength)
+            except:
+                raise EOFError
+
+    def _sendall(sock, data):
+        """
+        Writes data to a socket and does not return until all the data is sent.
+        """
+        length = len(data)
+        while length:
+            try:
+                sent = sock.send(data)
+            except socket.error, e:
+                if e[0] == errno.EPIPE:
+                    return # Don't bother raising an exception. Just ignore.
+                elif e[0] == errno.EAGAIN:
+                    select.select([], [sock], [])
+                    continue
+                else:
+                    raise
+            data = data[sent:]
+            length -= sent
+    _sendall = staticmethod(_sendall)
+
+    def write(self, sock):
+        """Encode and write a Record to a socket."""
+        self.paddingLength = -self.contentLength & 7
+
+        if __debug__: _debug(9, 'write: fd = %d, type = %d, requestId = %d, '
+                             'contentLength = %d' %
+                             (sock.fileno(), self.type, self.requestId,
+                              self.contentLength))
+
+        header = struct.pack(FCGI_Header, self.version, self.type,
+                             self.requestId, self.contentLength,
+                             self.paddingLength)
+        self._sendall(sock, header)
+        if self.contentLength:
+            self._sendall(sock, self.contentData)
+        if self.paddingLength:
+            self._sendall(sock, '\x00'*self.paddingLength)
+            
+class Request(object):
+    """
+    Represents a single FastCGI request.
+
+    These objects are passed to your handler and is the main interface
+    between your handler and the fcgi module. The methods should not
+    be called by your handler. However, server, params, stdin, stdout,
+    stderr, and data are free for your handler's use.
+    """
+    def __init__(self, conn, inputStreamClass):
+        self._conn = conn
+
+        self.server = conn.server
+        self.params = {}
+        self.stdin = inputStreamClass(conn)
+        self.stdout = OutputStream(conn, self, FCGI_STDOUT)
+        self.stderr = OutputStream(conn, self, FCGI_STDERR, buffered=True)
+        self.data = inputStreamClass(conn)
+
+    def run(self):
+        """Runs the handler, flushes the streams, and ends the request."""
+        try:
+            protocolStatus, appStatus = self.server.handler(self)
+        except:
+            traceback.print_exc(file=self.stderr)
+            self.stderr.flush()
+            if not self.stdout.dataWritten:
+                self.server.error(self)
+
+            protocolStatus, appStatus = FCGI_REQUEST_COMPLETE, 0
+
+        if __debug__: _debug(1, 'protocolStatus = %d, appStatus = %d' %
+                             (protocolStatus, appStatus))
+
+        self._flush()
+        self._end(appStatus, protocolStatus)
+
+    def _end(self, appStatus=0L, protocolStatus=FCGI_REQUEST_COMPLETE):
+        self._conn.end_request(self, appStatus, protocolStatus)
+        
+    def _flush(self):
+        self.stdout.close()
+        self.stderr.close()
+
+class CGIRequest(Request):
+    """A normal CGI request disguised as a FastCGI request."""
+    def __init__(self, server):
+        # These are normally filled in by Connection.
+        self.requestId = 1
+        self.role = FCGI_RESPONDER
+        self.flags = 0
+        self.aborted = False
+        
+        self.server = server
+        self.params = dict(os.environ)
+        self.stdin = sys.stdin
+        self.stdout = StdoutWrapper(sys.stdout) # Oh, the humanity!
+        self.stderr = sys.stderr
+        self.data = StringIO.StringIO()
+        
+    def _end(self, appStatus=0L, protocolStatus=FCGI_REQUEST_COMPLETE):
+        sys.exit(appStatus)
+
+    def _flush(self):
+        # Not buffered, do nothing.
+        pass
+
+class Connection(object):
+    """
+    A Connection with the web server.
+
+    Each Connection is associated with a single socket (which is
+    connected to the web server) and is responsible for handling all
+    the FastCGI message processing for that socket.
+    """
+    _multiplexed = False
+    _inputStreamClass = InputStream
+
+    def __init__(self, sock, addr, server):
+        self._sock = sock
+        self._addr = addr
+        self.server = server
+
+        # Active Requests for this Connection, mapped by request ID.
+        self._requests = {}
+
+    def _cleanupSocket(self):
+        """Close the Connection's socket."""
+        try:
+            self._sock.shutdown(socket.SHUT_WR)
+        except:
+            return
+        try:
+            while True:
+                r, w, e = select.select([self._sock], [], [])
+                if not r or not self._sock.recv(1024):
+                    break
+        except:
+            pass
+        self._sock.close()
+        
+    def run(self):
+        """Begin processing data from the socket."""
+        self._keepGoing = True
+        while self._keepGoing:
+            try:
+                self.process_input()
+            except EOFError:
+                break
+            except (select.error, socket.error), e:
+                if e[0] == errno.EBADF: # Socket was closed by Request.
+                    break
+                raise
+
+        self._cleanupSocket()
+
+    def process_input(self):
+        """Attempt to read a single Record from the socket and process it."""
+        # Currently, any children Request threads notify this Connection
+        # that it is no longer needed by closing the Connection's socket.
+        # We need to put a timeout on select, otherwise we might get
+        # stuck in it indefinitely... (I don't like this solution.)
+        while self._keepGoing:
+            try:
+                r, w, e = select.select([self._sock], [], [], 1.0)
+            except ValueError:
+                # Sigh. ValueError gets thrown sometimes when passing select
+                # a closed socket.
+                raise EOFError
+            if r: break
+        if not self._keepGoing:
+            return
+        rec = Record()
+        rec.read(self._sock)
+
+        if rec.type == FCGI_GET_VALUES:
+            self._do_get_values(rec)
+        elif rec.type == FCGI_BEGIN_REQUEST:
+            self._do_begin_request(rec)
+        elif rec.type == FCGI_ABORT_REQUEST:
+            self._do_abort_request(rec)
+        elif rec.type == FCGI_PARAMS:
+            self._do_params(rec)
+        elif rec.type == FCGI_STDIN:
+            self._do_stdin(rec)
+        elif rec.type == FCGI_DATA:
+            self._do_data(rec)
+        elif rec.requestId == FCGI_NULL_REQUEST_ID:
+            self._do_unknown_type(rec)
+        else:
+            # Need to complain about this.
+            pass
+
+    def writeRecord(self, rec):
+        """
+        Write a Record to the socket.
+        """
+        rec.write(self._sock)
+
+    def end_request(self, req, appStatus=0L,
+                    protocolStatus=FCGI_REQUEST_COMPLETE, remove=True):
+        """
+        End a Request.
+
+        Called by Request objects. An FCGI_END_REQUEST Record is
+        sent to the web server. If the web server no longer requires
+        the connection, the socket is closed, thereby ending this
+        Connection (run() returns).
+        """
+        rec = Record(FCGI_END_REQUEST, req.requestId)
+        rec.contentData = struct.pack(FCGI_EndRequestBody, appStatus,
+                                      protocolStatus)
+        rec.contentLength = FCGI_EndRequestBody_LEN
+        self.writeRecord(rec)
+
+        if remove:
+            del self._requests[req.requestId]
+
+        if __debug__: _debug(2, 'end_request: flags = %d' % req.flags)
+
+        if not (req.flags & FCGI_KEEP_CONN) and not self._requests:
+            self._cleanupSocket()
+            self._keepGoing = False
+
+    def _do_get_values(self, inrec):
+        """Handle an FCGI_GET_VALUES request from the web server."""
+        outrec = Record(FCGI_GET_VALUES_RESULT)
+
+        pos = 0
+        while pos < inrec.contentLength:
+            pos, (name, value) = decode_pair(inrec.contentData, pos)
+            cap = self.server.capability.get(name)
+            if cap is not None:
+                outrec.contentData += encode_pair(name, str(cap))
+
+        outrec.contentLength = len(outrec.contentData)
+        self.writeRecord(rec)
+
+    def _do_begin_request(self, inrec):
+        """Handle an FCGI_BEGIN_REQUEST from the web server."""
+        role, flags = struct.unpack(FCGI_BeginRequestBody, inrec.contentData)
+
+        req = self.server.request_class(self, self._inputStreamClass)
+        req.requestId, req.role, req.flags = inrec.requestId, role, flags
+        req.aborted = False
+
+        if not self._multiplexed and self._requests:
+            # Can't multiplex requests.
+            self.end_request(req, 0L, FCGI_CANT_MPX_CONN, remove=False)
+        else:
+            self._requests[inrec.requestId] = req
+
+    def _do_abort_request(self, inrec):
+        """
+        Handle an FCGI_ABORT_REQUEST from the web server.
+
+        We just mark a flag in the associated Request.
+        """
+        req = self._requests.get(inrec.requestId)
+        if req is not None:
+            req.aborted = True
+
+    def _start_request(self, req):
+        """Run the request."""
+        # Not multiplexed, so run it inline.
+        req.run()
+
+    def _do_params(self, inrec):
+        """
+        Handle an FCGI_PARAMS Record.
+
+        If the last FCGI_PARAMS Record is received, start the request.
+        """
+        req = self._requests.get(inrec.requestId)
+        if req is not None:
+            if inrec.contentLength:
+                pos = 0
+                while pos < inrec.contentLength:
+                    pos, (name, value) = decode_pair(inrec.contentData, pos)
+                    req.params[name] = value
+            else:
+                self._start_request(req)
+
+    def _do_stdin(self, inrec):
+        """Handle the FCGI_STDIN stream."""
+        req = self._requests.get(inrec.requestId)
+        if req is not None:
+            req.stdin.add_data(inrec.contentData)
+
+    def _do_data(self, inrec):
+        """Handle the FCGI_DATA stream."""
+        req = self._requests.get(inrec.requestId)
+        if req is not None:
+            req.data.add_data(inrec.contentData)
+
+    def _do_unknown_type(self, inrec):
+        """Handle an unknown request type. Respond accordingly."""
+        outrec = Record(FCGI_UNKNOWN_TYPE)
+        outrec.contentData = struct.pack(FCGI_UnknownTypeBody, inrec.type)
+        outrec.contentLength = FCGI_UnknownTypeBody_LEN
+        self.writeRecord(rec)
+        
+class MultiplexedConnection(Connection):
+    """
+    A version of Connection capable of handling multiple requests
+    simultaneously.
+    """
+    _multiplexed = True
+    _inputStreamClass = MultiplexedInputStream
+
+    def __init__(self, sock, addr, server):
+        super(MultiplexedConnection, self).__init__(sock, addr, server)
+
+        # Used to arbitrate access to self._requests.
+        lock = threading.RLock()
+
+        # Notification is posted everytime a request completes, allowing us
+        # to quit cleanly.
+        self._lock = threading.Condition(lock)
+
+    def _cleanupSocket(self):
+        # Wait for any outstanding requests before closing the socket.
+        self._lock.acquire()
+        while self._requests:
+            self._lock.wait()
+        self._lock.release()
+
+        super(MultiplexedConnection, self)._cleanupSocket()
+        
+    def writeRecord(self, rec):
+        # Must use locking to prevent intermingling of Records from different
+        # threads.
+        self._lock.acquire()
+        try:
+            # Probably faster than calling super. ;)
+            rec.write(self._sock)
+        finally:
+            self._lock.release()
+
+    def end_request(self, req, appStatus=0L,
+                    protocolStatus=FCGI_REQUEST_COMPLETE, remove=True):
+        self._lock.acquire()
+        try:
+            super(MultiplexedConnection, self).end_request(req, appStatus,
+                                                           protocolStatus,
+                                                           remove)
+            self._lock.notify()
+        finally:
+            self._lock.release()
+
+    def _do_begin_request(self, inrec):
+        self._lock.acquire()
+        try:
+            super(MultiplexedConnection, self)._do_begin_request(inrec)
+        finally:
+            self._lock.release()
+
+    def _do_abort_request(self, inrec):
+        self._lock.acquire()
+        try:
+            super(MultiplexedConnection, self)._do_abort_request(inrec)
+        finally:
+            self._lock.release()
+
+    def _start_request(self, req):
+        thread.start_new_thread(req.run, ())
+
+    def _do_params(self, inrec):
+        self._lock.acquire()
+        try:
+            super(MultiplexedConnection, self)._do_params(inrec)
+        finally:
+            self._lock.release()
+
+    def _do_stdin(self, inrec):
+        self._lock.acquire()
+        try:
+            super(MultiplexedConnection, self)._do_stdin(inrec)
+        finally:
+            self._lock.release()
+
+    def _do_data(self, inrec):
+        self._lock.acquire()
+        try:
+            super(MultiplexedConnection, self)._do_data(inrec)
+        finally:
+            self._lock.release()
+        
+class Server(object):
+    """
+    The FastCGI server.
+
+    Waits for connections from the web server, processing each
+    request.
+
+    If run in a normal CGI context, it will instead instantiate a
+    CGIRequest and run the handler through there.
+    """
+    request_class = Request
+    cgirequest_class = CGIRequest
+
+    # Limits the size of the InputStream's string buffer to this size + the
+    # server's maximum Record size. Since the InputStream is not seekable,
+    # we throw away already-read data once this certain amount has been read.
+    inputStreamShrinkThreshold = 102400 - 8192
+
+    def __init__(self, handler=None, maxwrite=8192, bindAddress=None,
+                 multiplexed=False):
+        """
+        handler, if present, must reference a function or method that
+        takes one argument: a Request object. If handler is not
+        specified at creation time, Server *must* be subclassed.
+        (The handler method below is abstract.)
+
+        maxwrite is the maximum number of bytes (per Record) to write
+        to the server. I've noticed mod_fastcgi has a relatively small
+        receive buffer (8K or so).
+
+        bindAddress, if present, must either be a string or a 2-tuple. If
+        present, run() will open its own listening socket. You would use
+        this if you wanted to run your application as an 'external' FastCGI
+        app. (i.e. the webserver would no longer be responsible for starting
+        your app) If a string, it will be interpreted as a filename and a UNIX
+        socket will be opened. If a tuple, the first element, a string,
+        is the interface name/IP to bind to, and the second element (an int)
+        is the port number.
+
+        Set multiplexed to True if you want to handle multiple requests
+        per connection. Some FastCGI backends (namely mod_fastcgi) don't
+        multiplex requests at all, so by default this is off (which saves
+        on thread creation/locking overhead). If threads aren't available,
+        this keyword is ignored; it's not possible to multiplex requests
+        at all.
+        """
+        if handler is not None:
+            self.handler = handler
+        self.maxwrite = maxwrite
+        if thread_available:
+            try:
+                import resource
+                # Attempt to glean the maximum number of connections
+                # from the OS.
+                maxConns = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
+            except ImportError:
+                maxConns = 100 # Just some made up number.
+            maxReqs = maxConns
+            if multiplexed:
+                self._connectionClass = MultiplexedConnection
+                maxReqs *= 5 # Another made up number.
+            else:
+                self._connectionClass = Connection
+            self.capability = {
+                FCGI_MAX_CONNS: maxConns,
+                FCGI_MAX_REQS: maxReqs,
+                FCGI_MPXS_CONNS: multiplexed and 1 or 0
+                }
+        else:
+            self._connectionClass = Connection
+            self.capability = {
+                # If threads aren't available, these are pretty much correct.
+                FCGI_MAX_CONNS: 1,
+                FCGI_MAX_REQS: 1,
+                FCGI_MPXS_CONNS: 0
+                }
+        self._bindAddress = bindAddress
+
+    def _setupSocket(self):
+        if self._bindAddress is None: # Run as a normal FastCGI?
+            isFCGI = True
+
+            sock = socket.fromfd(FCGI_LISTENSOCK_FILENO, socket.AF_INET,
+                                 socket.SOCK_STREAM)
+            try:
+                sock.getpeername()
+            except socket.error, e:
+                if e[0] == errno.ENOTSOCK:
+                    # Not a socket, assume CGI context.
+                    isFCGI = False
+                elif e[0] != errno.ENOTCONN:
+                    raise
+
+            # FastCGI/CGI discrimination is broken on Mac OS X.
+            # Set the environment variable FCGI_FORCE_CGI to "Y" or "y"
+            # if you want to run your app as a simple CGI. (You can do
+            # this with Apache's mod_env [not loaded by default in OS X
+            # client, ha ha] and the SetEnv directive.)
+            if not isFCGI or \
+               os.environ.get('FCGI_FORCE_CGI', 'N').upper().startswith('Y'):
+                req = self.cgirequest_class(self)
+                req.run()
+                sys.exit(0)
+        else:
+            # Run as a server
+            if type(self._bindAddress) is str:
+                # Unix socket
+                sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+                try:
+                    os.unlink(self._bindAddress)
+                except OSError:
+                    pass
+            else:
+                # INET socket
+                assert type(self._bindAddress) is tuple
+                assert len(self._bindAddress) == 2
+                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+                sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+
+            sock.bind(self._bindAddress)
+            sock.listen(socket.SOMAXCONN)
+
+        return sock
+
+    def _cleanupSocket(self, sock):
+        """Closes the main socket."""
+        sock.close()
+
+    def _installSignalHandlers(self):
+        self._oldSIGs = [(x,signal.getsignal(x)) for x in
+                         (signal.SIGHUP, signal.SIGINT, signal.SIGTERM)]
+        signal.signal(signal.SIGHUP, self._hupHandler)
+        signal.signal(signal.SIGINT, self._intHandler)
+        signal.signal(signal.SIGTERM, self._intHandler)
+
+    def _restoreSignalHandlers(self):
+        for signum,handler in self._oldSIGs:
+            signal.signal(signum, handler)
+        
+    def _hupHandler(self, signum, frame):
+        self._hupReceived = True
+        self._keepGoing = False
+
+    def _intHandler(self, signum, frame):
+        self._keepGoing = False
+
+    def run(self, timeout=1.0):
+        """
+        The main loop. Exits on SIGHUP, SIGINT, SIGTERM. Returns True if
+        SIGHUP was received, False otherwise.
+        """
+        web_server_addrs = os.environ.get('FCGI_WEB_SERVER_ADDRS')
+        if web_server_addrs is not None:
+            web_server_addrs = map(lambda x: x.strip(),
+                                   web_server_addrs.split(','))
+
+        sock = self._setupSocket()
+
+        self._keepGoing = True
+        self._hupReceived = False
+
+        # Install signal handlers.
+        self._installSignalHandlers()
+
+        while self._keepGoing:
+            try:
+                r, w, e = select.select([sock], [], [], timeout)
+            except select.error, e:
+                if e[0] == errno.EINTR:
+                    continue
+                raise
+
+            if r:
+                try:
+                    clientSock, addr = sock.accept()
+                except socket.error, e:
+                    if e[0] in (errno.EINTR, errno.EAGAIN):
+                        continue
+                    raise
+
+                if web_server_addrs and \
+                       (len(addr) != 2 or addr[0] not in web_server_addrs):
+                    clientSock.close()
+                    continue
+
+                # Instantiate a new Connection and begin processing FastCGI
+                # messages (either in a new thread or this thread).
+                conn = self._connectionClass(clientSock, addr, self)
+                thread.start_new_thread(conn.run, ())
+
+            self._mainloopPeriodic()
+
+        # Restore signal handlers.
+        self._restoreSignalHandlers()
+
+        self._cleanupSocket(sock)
+
+        return self._hupReceived
+
+    def _mainloopPeriodic(self):
+        """
+        Called with just about each iteration of the main loop. Meant to
+        be overridden.
+        """
+        pass
+
+    def _exit(self, reload=False):
+        """
+        Protected convenience method for subclasses to force an exit. Not
+        really thread-safe, which is why it isn't public.
+        """
+        if self._keepGoing:
+            self._keepGoing = False
+            self._hupReceived = reload
+
+    def handler(self, req):
+        """
+        Default handler, which just raises an exception. Unless a handler
+        is passed at initialization time, this must be implemented by
+        a subclass.
+        """
+        raise NotImplementedError, self.__class__.__name__ + '.handler'
+
+    def error(self, req):
+        """
+        Called by Request if an exception occurs within the handler. May and
+        should be overridden.
+        """
+        import cgitb
+        req.stdout.write('Content-Type: text/html\r\n\r\n' +
+                         cgitb.html(sys.exc_info()))
+
+class WSGIServer(Server):
+    """
+    FastCGI server that supports the Web Server Gateway Interface. See
+    <http://www.python.org/peps/pep-0333.html>.
+    """
+    def __init__(self, application, environ=None, multithreaded=True, **kw):
+        """
+        environ, if present, must be a dictionary-like object. Its
+        contents will be copied into application's environ. Useful
+        for passing application-specific variables.
+
+        Set multithreaded to False if your application is not MT-safe.
+        """
+        if kw.has_key('handler'):
+            del kw['handler'] # Doesn't make sense to let this through
+        super(WSGIServer, self).__init__(**kw)
+
+        if environ is None:
+            environ = {}
+
+        self.application = application
+        self.environ = environ
+        self.multithreaded = multithreaded
+
+        # Used to force single-threadedness
+        self._app_lock = thread.allocate_lock()
+
+    def handler(self, req):
+        """Special handler for WSGI."""
+        if req.role != FCGI_RESPONDER:
+            return FCGI_UNKNOWN_ROLE, 0
+
+        # Mostly taken from example CGI gateway.
+        environ = req.params
+        environ.update(self.environ)
+
+        environ['wsgi.version'] = (1,0)
+        environ['wsgi.input'] = req.stdin
+        if self._bindAddress is None:
+            stderr = req.stderr
+        else:
+            stderr = TeeOutputStream((sys.stderr, req.stderr))
+        environ['wsgi.errors'] = stderr
+        environ['wsgi.multithread'] = not isinstance(req, CGIRequest) and \
+                                      thread_available and self.multithreaded
+        # Rationale for the following: If started by the web server
+        # (self._bindAddress is None) in either FastCGI or CGI mode, the
+        # possibility of being spawned multiple times simultaneously is quite
+        # real. And, if started as an external server, multiple copies may be
+        # spawned for load-balancing/redundancy. (Though I don't think
+        # mod_fastcgi supports this?)
+        environ['wsgi.multiprocess'] = True
+        environ['wsgi.run_once'] = isinstance(req, CGIRequest)
+
+        if environ.get('HTTPS', 'off') in ('on', '1'):
+            environ['wsgi.url_scheme'] = 'https'
+        else:
+            environ['wsgi.url_scheme'] = 'http'
+
+        self._sanitizeEnv(environ)
+
+        headers_set = []
+        headers_sent = []
+        result = None
+
+        def write(data):
+            assert type(data) is str, 'write() argument must be string'
+            assert headers_set, 'write() before start_response()'
+
+            if not headers_sent:
+                status, responseHeaders = headers_sent[:] = headers_set
+                found = False
+                for header,value in responseHeaders:
+                    if header.lower() == 'content-length':
+                        found = True
+                        break
+                if not found and result is not None:
+                    try:
+                        if len(result) == 1:
+                            responseHeaders.append(('Content-Length',
+                                                    str(len(data))))
+                    except:
+                        pass
+                s = 'Status: %s\r\n' % status
+                for header in responseHeaders:
+                    s += '%s: %s\r\n' % header
+                s += '\r\n'
+                req.stdout.write(s)
+
+            req.stdout.write(data)
+            req.stdout.flush()
+
+        def start_response(status, response_headers, exc_info=None):
+            if exc_info:
+                try:
+                    if headers_sent:
+                        # Re-raise if too late
+                        raise exc_info[0], exc_info[1], exc_info[2]
+                finally:
+                    exc_info = None # avoid dangling circular ref
+            else:
+                assert not headers_set, 'Headers already set!'
+
+            assert type(status) is str, 'Status must be a string'
+            assert len(status) >= 4, 'Status must be at least 4 characters'
+            assert int(status[:3]), 'Status must begin with 3-digit code'
+            assert status[3] == ' ', 'Status must have a space after code'
+            assert type(response_headers) is list, 'Headers must be a list'
+            if __debug__:
+                for name,val in response_headers:
+                    assert type(name) is str, 'Header names must be strings'
+                    assert type(val) is str, 'Header values must be strings'
+
+            headers_set[:] = [status, response_headers]
+            return write
+
+        if not self.multithreaded:
+            self._app_lock.acquire()
+        try:
+            result = self.application(environ, start_response)
+            try:
+                for data in result:
+                    if data:
+                        write(data)
+                if not headers_sent:
+                    write('') # in case body was empty
+            finally:
+                if hasattr(result, 'close'):
+                    result.close()
+        finally:
+            if not self.multithreaded:
+                self._app_lock.release()
+
+        return FCGI_REQUEST_COMPLETE, 0
+
+    def _sanitizeEnv(self, environ):
+        """Ensure certain values are present, if required by WSGI."""
+        if not environ.has_key('SCRIPT_NAME'):
+            environ['SCRIPT_NAME'] = ''
+        if not environ.has_key('PATH_INFO'):
+            environ['PATH_INFO'] = ''
+
+        # If any of these are missing, it probably signifies a broken
+        # server...
+        for name,default in [('REQUEST_METHOD', 'GET'),
+                             ('SERVER_NAME', 'localhost'),
+                             ('SERVER_PORT', '80'),
+                             ('SERVER_PROTOCOL', 'HTTP/1.0')]:
+            if not environ.has_key(name):
+                environ['wsgi.errors'].write('%s: missing FastCGI param %s '
+                                             'required by WSGI!\n' %
+                                             (self.__class__.__name__, name))
+                environ[name] = default
+            
+if __name__ == '__main__':
+    def test_app(environ, start_response):
+        """Probably not the most efficient example."""
+        import cgi
+        start_response('200 OK', [('Content-Type', 'text/html')])
+        yield '<html><head><title>Hello World!</title></head>\n' \
+              '<body>\n' \
+              '<p>Hello World!</p>\n' \
+              '<table border="1">'
+        names = environ.keys()
+        names.sort()
+        for name in names:
+            yield '<tr><td>%s</td><td>%s</td></tr>\n' % (
+                name, cgi.escape(`environ[name]`))
+
+        form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ,
+                                keep_blank_values=1)
+        if form.list:
+            yield '<tr><th colspan="2">Form data</th></tr>'
+
+        for field in form.list:
+            yield '<tr><td>%s</td><td>%s</td></tr>\n' % (
+                field.name, field.value)
+
+        yield '</table>\n' \
+              '</body></html>\n'
+
+    WSGIServer(test_app).run()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/api.py
@@ -0,0 +1,482 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+from BaseHTTPServer import BaseHTTPRequestHandler
+from Cookie import SimpleCookie as Cookie
+import cgi
+import mimetypes
+import os
+from StringIO import StringIO
+import sys
+import urlparse
+
+from trac.core import Interface
+from trac.util import get_last_traceback
+from trac.util.datefmt import http_date
+from trac.web.href import Href
+
+HTTP_STATUS = dict([(code, reason.title()) for code, (reason, description)
+                    in BaseHTTPRequestHandler.responses.items()])
+
+
+class HTTPException(Exception):
+    """Exception representing a HTTP status code."""
+
+    def __init__(self, code):
+        self.code = code
+        self.reason = HTTP_STATUS.get(self.code, 'Unknown')
+        self.__doc__ = 'Exception for HTTP %d %s' % (self.code, self.reason)
+
+    def __call__(self, message, *args):
+        self.message = message
+        if args:
+            self.message = self.message % args
+        Exception.__init__(self, '%s %s (%s)' % (self.code, self.reason,
+                                                 message))
+        return self
+
+    def __str__(self):
+        return '%s %s (%s)' % (self.code, self.reason, self.message)
+
+
+for code in [code for code in HTTP_STATUS if code >= 400]:
+    exc_name = HTTP_STATUS[code].replace(' ', '')
+    if exc_name.lower().startswith('http'):
+        exc_name = exc_name[4:]
+    setattr(sys.modules[__name__], 'HTTP' + exc_name, HTTPException(code))
+del code, exc_name
+
+
+class _RequestArgs(dict):
+    """Dictionary subclass that provides convenient access to request
+    parameters that may contain multiple values."""
+
+    def getfirst(self, name, default=None):
+        """Return the first value for the specified parameter, or `default` if
+        the parameter was not provided.
+        """
+        if name not in self:
+            return default
+        val = self[name]
+        if isinstance(val, list):
+            val = val[0]
+        return val
+
+    def getlist(self, name):
+        """Return a list of values for the specified parameter, even if only
+        one value was provided.
+        """
+        if name not in self:
+            return []
+        val = self[name]
+        if not isinstance(val, list):
+            val = [val]
+        return val
+
+
+class RequestDone(Exception):
+    """Marker exception that indicates whether request processing has completed
+    and a response was sent.
+    """
+
+
+class Request(object):
+    """Represents a HTTP request/response pair.
+    
+    This class provides a convenience API over WSGI.
+    """
+    args = None
+    hdf = None
+    authname = None
+    perm = None
+    session = None
+
+    def __init__(self, environ, start_response):
+        """Create the request wrapper.
+        
+        @param environ: The WSGI environment dict
+        @param start_response: The WSGI callback for starting the response
+        """
+        self.environ = environ
+        self._start_response = start_response
+        self._write = None
+        self._status = '200 OK'
+        self._response = None
+
+        self._inheaders = [(name[5:].replace('_', '-').lower(), value)
+                           for name, value in environ.items()
+                           if name.startswith('HTTP_')]
+        if 'CONTENT_LENGTH' in environ:
+            self._inheaders.append(('content-length',
+                                    environ['CONTENT_LENGTH']))
+        if 'CONTENT_TYPE' in environ:
+            self._inheaders.append(('content-type', environ['CONTENT_TYPE']))
+        self._outheaders = []
+        self._outcharset = None
+
+        self.incookie = Cookie()
+        cookie = self.get_header('Cookie')
+        if cookie:
+            self.incookie.load(cookie)
+        self.outcookie = Cookie()
+
+        self.base_url = self.environ.get('trac.base_url')
+        if not self.base_url:
+            self.base_url = self._reconstruct_url()
+        self.href = Href(self.base_path)
+        self.abs_href = Href(self.base_url)
+
+        self.args = self._parse_args()
+
+    def _parse_args(self):
+        """Parse the supplied request parameters into a dictionary."""
+        args = _RequestArgs()
+
+        fp = self.environ['wsgi.input']
+        ctype = self.get_header('Content-Type')
+        if ctype:
+            # Avoid letting cgi.FieldStorage consume the input stream when the
+            # request does not contain form data
+            ctype, options = cgi.parse_header(ctype)
+            if ctype not in ('application/x-www-form-urlencoded',
+                             'multipart/form-data'):
+                fp = StringIO('')
+
+        fs = cgi.FieldStorage(fp, environ=self.environ, keep_blank_values=True)
+        if fs.list:
+            for name in fs.keys():
+                values = fs[name]
+                if not isinstance(values, list):
+                    values = [values]
+                for value in values:
+                    if not value.filename:
+                        value = unicode(value.value, 'utf-8')
+                    if name in args:
+                        if isinstance(args[name], list):
+                            args[name].append(value)
+                        else:
+                            args[name] = [args[name], value]
+                    else:
+                        args[name] = value
+
+        return args
+
+    def _reconstruct_url(self):
+        """Reconstruct the absolute base URL of the application."""
+        host = self.get_header('Host')
+        if not host:
+            # Missing host header, so reconstruct the host from the
+            # server name and port
+            default_port = {'http': 80, 'https': 443}
+            if self.server_port and self.server_port != default_port[self.scheme]:
+                host = '%s:%d' % (self.server_name, self.server_port)
+            else:
+                host = self.server_name
+        return urlparse.urlunparse((self.scheme, host, self.base_path, None,
+                                    None, None))
+
+    method = property(fget=lambda self: self.environ['REQUEST_METHOD'],
+                      doc='The HTTP method of the request')
+    path_info = property(fget=lambda self: self.environ.get('PATH_INFO', '').decode('utf-8'),
+                         doc='Path inside the application')
+    remote_addr = property(fget=lambda self: self.environ.get('REMOTE_ADDR'),
+                           doc='IP address of the remote user')
+    remote_user = property(fget=lambda self: self.environ.get('REMOTE_USER'),
+                           doc='Name of the remote user, `None` if the user'
+                               'has not logged in using HTTP authentication')
+    scheme = property(fget=lambda self: self.environ['wsgi.url_scheme'],
+                      doc='The scheme of the request URL')
+    base_path = property(fget=lambda self: self.environ.get('SCRIPT_NAME', ''),
+                         doc='The root path of the application')
+    server_name = property(fget=lambda self: self.environ['SERVER_NAME'],
+                           doc='Name of the server')
+    server_port = property(fget=lambda self: int(self.environ['SERVER_PORT']),
+                           doc='Port number the server is bound to')
+
+    def get_header(self, name):
+        """Return the value of the specified HTTP header, or `None` if there's
+        no such header in the request.
+        """
+        name = name.lower()
+        for key, value in self._inheaders:
+            if key == name:
+                return value
+        return None
+
+    def send_response(self, code=200):
+        """Set the status code of the response."""
+        self._status = '%s %s' % (code, HTTP_STATUS.get(code, 'Unknown'))
+
+    def send_header(self, name, value):
+        """Send the response header with the specified name and value.
+
+        `value` must either be an `unicode` string or can be converted to one
+        (e.g. numbers, ...)
+        """
+        if name.lower() == 'content-type':
+            ctpos = value.find('charset=')
+            if ctpos >= 0:
+                self._outcharset = value[ctpos + 8:].strip()
+        self._outheaders.append((name, unicode(value).encode('utf-8')))
+
+    def _send_cookie_headers(self):
+        for name in self.outcookie.keys():
+            path = self.outcookie[name].get('path')
+            if path:
+                path = path.replace(' ', '%20') \
+                           .replace(';', '%3B') \
+                           .replace(',', '%3C')
+            self.outcookie[name]['path'] = path
+
+        cookies = self.outcookie.output(header='')
+        for cookie in cookies.splitlines():
+            self._outheaders.append(('Set-Cookie', cookie.strip()))
+
+    def end_headers(self):
+        """Must be called after all headers have been sent and before the actual
+        content is written.
+        """
+        self._send_cookie_headers()
+        self._write = self._start_response(self._status, self._outheaders)
+
+    def check_modified(self, timesecs, extra=''):
+        """Check the request "If-None-Match" header against an entity tag.
+
+        The entity tag is generated from the specified last modified time
+        in seconds (`timesecs`), optionally appending an `extra` string to
+        indicate variants of the requested resource.
+
+        That `extra` parameter can also be a list, in which case the MD5 sum
+        of the list content will be used.
+
+        If the generated tag matches the "If-None-Match" header of the request,
+        this method sends a "304 Not Modified" response to the client.
+        Otherwise, it adds the entity tag as an "ETag" header to the response
+        so that consecutive requests can be cached.
+        """
+        if isinstance(extra, list):
+            import md5
+            m = md5.new()
+            for elt in extra:
+                m.update(repr(elt))
+            extra = m.hexdigest()
+        etag = 'W"%s/%d/%s"' % (self.authname, timesecs, extra)
+        inm = self.get_header('If-None-Match')
+        if (not inm or inm != etag):
+            self.send_header('ETag', etag)
+        else:
+            self.send_response(304)
+            self.end_headers()
+            raise RequestDone
+
+    def redirect(self, url, permanent=False):
+        """Send a redirect to the client, forwarding to the specified URL. The
+        `url` may be relative or absolute, relative URLs will be translated
+        appropriately.
+        """
+        if self.session:
+            self.session.save() # has to be done before the redirect is sent
+
+        if permanent:
+            status = 301 # 'Moved Permanently'
+        elif self.method == 'POST':
+            status = 303 # 'See Other' -- safe to use in response to a POST
+        else:
+            status = 302 # 'Found' -- normal temporary redirect
+
+        self.send_response(status)
+        if not url.startswith('http://') and not url.startswith('https://'):
+            # Make sure the URL is absolute
+            url = urlparse.urlunparse((self.scheme,
+                                       urlparse.urlparse(self.base_url)[1],
+                                       url, None, None, None))
+        self.send_header('Location', url)
+        self.send_header('Content-Type', 'text/plain')
+        self.send_header('Pragma', 'no-cache')
+        self.send_header('Cache-control', 'no-cache')
+        self.send_header('Expires', 'Fri, 01 Jan 1999 00:00:00 GMT')
+        self.end_headers()
+
+        if self.method != 'HEAD':
+            self.write('Redirecting...')
+        raise RequestDone
+
+    def display(self, template, content_type='text/html', status=200):
+        """Render the response using the ClearSilver template given by the
+        `template` parameter, which can be either the name of the template file,
+        or an already parsed `neo_cs.CS` object.
+        """
+        assert self.hdf, 'HDF dataset not available'
+        if self.args.has_key('hdfdump'):
+            # FIXME: the administrator should probably be able to disable HDF
+            #        dumps
+            content_type = 'text/plain'
+            data = str(self.hdf)
+        else:
+            data = self.hdf.render(template)
+
+        self.send_response(status)
+        self.send_header('Cache-control', 'must-revalidate')
+        self.send_header('Expires', 'Fri, 01 Jan 1999 00:00:00 GMT')
+        self.send_header('Content-Type', content_type + ';charset=utf-8')
+        self.send_header('Content-Length', len(data))
+        self.end_headers()
+
+        if self.method != 'HEAD':
+            self.write(data)
+        raise RequestDone
+
+    def send_error(self, exc_info, template='error.cs',
+                   content_type='text/html', status=500):
+        if self.hdf:
+            if self.args.has_key('hdfdump'):
+                # FIXME: the administrator should probably be able to disable HDF
+                #        dumps
+                content_type = 'text/plain'
+                data = str(self.hdf)
+            else:
+                data = self.hdf.render(template)
+        else:
+            content_type = 'text/plain'
+            data = get_last_traceback()
+
+        self.send_response(status)
+        self._outheaders = []
+        self.send_header('Cache-control', 'must-revalidate')
+        self.send_header('Expires', 'Fri, 01 Jan 1999 00:00:00 GMT')
+        self.send_header('Content-Type', content_type + ';charset=utf-8')
+        self.send_header('Content-Length', len(data))
+        self._send_cookie_headers()
+
+        self._write = self._start_response(self._status, self._outheaders,
+                                           exc_info)
+
+        if self.method != 'HEAD':
+            self.write(data)
+        raise RequestDone
+
+    def send_file(self, path, mimetype=None):
+        """Send a local file to the browser.
+        
+        This method includes the "Last-Modified", "Content-Type" and
+        "Content-Length" headers in the response, corresponding to the file
+        attributes. It also checks the last modification time of the local file
+        against the "If-Modified-Since" provided by the user agent, and sends a
+        "304 Not Modified" response if it matches.
+        """
+        if not os.path.isfile(path):
+            raise HTTPNotFound("File %s not found" % path)
+
+        stat = os.stat(path)
+        last_modified = http_date(stat.st_mtime)
+        if last_modified == self.get_header('If-Modified-Since'):
+            self.send_response(304)
+            self.end_headers()
+            raise RequestDone
+
+        if not mimetype:
+            mimetype = mimetypes.guess_type(path)[0] or \
+                       'application/octet-stream'
+
+        self.send_response(200)
+        self.send_header('Content-Type', mimetype)
+        self.send_header('Content-Length', stat.st_size)
+        self.send_header('Last-Modified', last_modified)
+        self.end_headers()
+
+        if self.method != 'HEAD':
+            self._response = file(path, 'rb')
+            file_wrapper = self.environ.get('wsgi.file_wrapper')
+            if file_wrapper:
+                self._response = file_wrapper(self._response, 4096)
+        raise RequestDone
+
+    def read(self, size=None):
+        """Read the specified number of bytes from the request body."""
+        fileobj = self.environ['wsgi.input']
+        if size is None:
+            size = int(self.get_header('Content-Length', -1))
+        data = fileobj.read(size)
+        return data
+
+    def write(self, data):
+        """Write the given data to the response body.
+
+        `data` can be either a `str` or an `unicode` string.
+        If it's the latter, the unicode string will be encoded
+        using the charset specified in the ''Content-Type'' header
+        or 'utf-8' otherwise.
+        """
+        if not self._write:
+            self.end_headers()
+        if isinstance(data, unicode):
+            data = data.encode(self._outcharset or 'utf-8')
+        self._write(data)
+
+
+class IAuthenticator(Interface):
+    """Extension point interface for components that can provide the name
+    of the remote user."""
+
+    def authenticate(req):
+        """Return the name of the remote user, or `None` if the identity of the
+        user is unknown."""
+
+
+class IRequestHandler(Interface):
+    """Extension point interface for request handlers."""
+
+    # implementing classes should set this property to `True` if they
+    # don't need session and authentication related information
+    anonymous_request = False
+    
+    # implementing classes should set this property to `False` if they
+    # don't need the HDF data and don't produce content using a template
+    use_template = True
+    
+    def match_request(req):
+        """Return whether the handler wants to process the given request."""
+
+    def process_request(req):
+        """Process the request. Should return a (template_name, content_type)
+        tuple, where `template` is the ClearSilver template to use (either
+        a `neo_cs.CS` object, or the file name of the template), and
+        `content_type` is the MIME type of the content. If `content_type` is
+        `None`, "text/html" is assumed.
+
+        Note that if template processing should not occur, this method can
+        simply send the response itself and not return anything.
+        """
+
+
+class IRequestFilter(Interface):
+    """Extension point interface for components that want to filter HTTP
+    requests, before and/or after they are processed by the main handler."""
+
+    def pre_process_request(req, handler):
+        """Do any pre-processing the request might need; typically adding
+        values to req.hdf, or redirecting.
+        
+        Always returns the request handler, even if unchanged.
+        """
+
+    def post_process_request(req, template, content_type):
+        """Do any post-processing the request might need; typically adding
+        values to req.hdf, or changing template or mime type.
+        
+        Always returns a tuple of (template, content_type), even if
+        unchanged.
+        """
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/auth.py
@@ -0,0 +1,327 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+
+try:
+    from base64 import b64decode
+except ImportError:
+    from base64 import decodestring as b64decode
+import md5
+import re
+import sys
+import time
+import urllib2
+
+from trac.config import BoolOption
+from trac.core import *
+from trac.web.api import IAuthenticator, IRequestHandler
+from trac.web.chrome import INavigationContributor
+from trac.util import hex_entropy, md5crypt
+from trac.util.markup import escape, html
+
+
+class LoginModule(Component):
+    """Implements user authentication based on HTTP authentication provided by
+    the web-server, combined with cookies for communicating the login
+    information across the whole site.
+
+    This mechanism expects that the web-server is setup so that a request to the
+    path '/login' requires authentication (such as Basic or Digest). The login
+    name is then stored in the database and associated with a unique key that
+    gets passed back to the user agent using the 'trac_auth' cookie. This cookie
+    is used to identify the user in subsequent requests to non-protected
+    resources.
+    """
+
+    implements(IAuthenticator, INavigationContributor, IRequestHandler)
+
+    check_ip = BoolOption('trac', 'check_auth_ip', 'true',
+         """Whether the IP address of the user should be checked for
+         authentication (''since 0.9'').""")
+
+    ignore_case = BoolOption('trac', 'ignore_auth_case', 'false',
+        """Whether case should be ignored for login names (''since 0.9'').""")
+
+    # IAuthenticator methods
+
+    def authenticate(self, req):
+        authname = None
+        if req.remote_user:
+            authname = req.remote_user
+        elif req.incookie.has_key('trac_auth'):
+            authname = self._get_name_for_cookie(req, req.incookie['trac_auth'])
+
+        if not authname:
+            return None
+
+        if self.ignore_case:
+            authname = authname.lower()
+
+        return authname
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'login'
+
+    def get_navigation_items(self, req):
+        if req.authname and req.authname != 'anonymous':
+            yield ('metanav', 'login', 'logged in as %s' % req.authname)
+            yield ('metanav', 'logout',
+                   html.A('Logout', href=req.href.logout()))
+        else:
+            yield ('metanav', 'login',
+                   html.A('Login', href=req.href.login()))
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        return re.match('/(login|logout)/?', req.path_info)
+
+    def process_request(self, req):
+        if req.path_info.startswith('/login'):
+            self._do_login(req)
+        elif req.path_info.startswith('/logout'):
+            self._do_logout(req)
+        self._redirect_back(req)
+
+    # Internal methods
+
+    def _do_login(self, req):
+        """Log the remote user in.
+
+        This function expects to be called when the remote user name is
+        available. The user name is inserted into the `auth_cookie` table and a
+        cookie identifying the user on subsequent requests is sent back to the
+        client.
+
+        If the Authenticator was created with `ignore_case` set to true, then 
+        the authentication name passed from the web server in req.remote_user
+        will be converted to lower case before being used. This is to avoid
+        problems on installations authenticating against Windows which is not
+        case sensitive regarding user names and domain names
+        """
+        assert req.remote_user, 'Authentication information not available.'
+
+        remote_user = req.remote_user
+        if self.ignore_case:
+            remote_user = remote_user.lower()
+
+        assert req.authname in ('anonymous', remote_user), \
+               'Already logged in as %s.' % req.authname
+
+        cookie = hex_entropy()
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("INSERT INTO auth_cookie (cookie,name,ipnr,time) "
+                       "VALUES (%s, %s, %s, %s)", (cookie, remote_user,
+                       req.remote_addr, int(time.time())))
+        db.commit()
+
+        req.authname = remote_user
+        req.outcookie['trac_auth'] = cookie
+        req.outcookie['trac_auth']['path'] = req.href()
+
+    def _do_logout(self, req):
+        """Log the user out.
+
+        Simply deletes the corresponding record from the auth_cookie table.
+        """
+        if req.authname == 'anonymous':
+            # Not logged in
+            return
+
+        # While deleting this cookie we also take the opportunity to delete
+        # cookies older than 10 days
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("DELETE FROM auth_cookie WHERE name=%s OR time < %s",
+                       (req.authname, int(time.time()) - 86400 * 10))
+        db.commit()
+        self._expire_cookie(req)
+
+    def _expire_cookie(self, req):
+        """Instruct the user agent to drop the auth cookie by setting the
+        "expires" property to a date in the past.
+        """
+        req.outcookie['trac_auth'] = ''
+        req.outcookie['trac_auth']['path'] = req.href()
+        req.outcookie['trac_auth']['expires'] = -10000
+
+    def _get_name_for_cookie(self, req, cookie):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        if self.check_ip:
+            cursor.execute("SELECT name FROM auth_cookie "
+                           "WHERE cookie=%s AND ipnr=%s",
+                           (cookie.value, req.remote_addr))
+        else:
+            cursor.execute("SELECT name FROM auth_cookie WHERE cookie=%s",
+                           (cookie.value,))
+        row = cursor.fetchone()
+        if not row:
+            # The cookie is invalid (or has been purged from the database), so
+            # tell the user agent to drop it as it is invalid
+            self._expire_cookie(req)
+            return None
+
+        return row[0]
+
+    def _redirect_back(self, req):
+        """Redirect the user back to the URL she came from."""
+        referer = req.get_header('Referer')
+        if referer and not referer.startswith(req.base_url):
+            # only redirect to referer if it is from the same site
+            referer = None
+        req.redirect(referer or req.abs_href())
+
+
+class HTTPAuthentication(object):
+
+    def do_auth(self, environ, start_response):
+        raise NotImplementedError
+
+
+class BasicAuthentication(HTTPAuthentication):
+
+    def __init__(self, htpasswd, realm):
+        self.hash = {}
+        self.realm = realm
+        try:
+            import crypt
+            self.crypt = crypt.crypt
+        except ImportError:
+            self.crypt = None
+        self.load(htpasswd)
+
+    def load(self, filename):
+        fd = open(filename, 'r')
+        for line in fd:
+            u, h = line.strip().split(':')
+            if '$' in h or self.crypt:
+                self.hash[u] = h
+            else:
+                print >>sys.stderr, 'Warning: cannot parse password for ' \
+                                    'user "%s" without the "crypt" module' % u
+
+        if self.hash == {}:
+            print >> sys.stderr, "Warning: found no users in file:", filename
+
+    def test(self, user, password):
+        the_hash = self.hash.get(user)
+        if the_hash is None:
+            return False
+
+        if not '$' in the_hash:
+            return self.crypt(password, the_hash[:2]) == the_hash
+
+        magic, salt = the_hash[1:].split('$')[:2]
+        magic = '$' + magic + '$'
+        return md5crypt(password, salt, magic) == the_hash
+
+    def do_auth(self, environ, start_response):
+        header = environ.get('HTTP_AUTHORIZATION')
+        if header and header.startswith('Basic'):
+            auth = b64decode(header[6:]).split(':')
+            if len(auth) == 2:
+                user, password = auth
+                if self.test(user, password):
+                    return user
+
+        start_response('401 Unauthorized',
+                       [('WWW-Authenticate', 'Basic realm="%s"'
+                         % self.realm)])('')
+
+
+class DigestAuthentication(HTTPAuthentication):
+    """A simple HTTP digest authentication implementation (RFC 2617)."""
+
+    MAX_NONCES = 100
+
+    def __init__(self, htdigest, realm):
+        self.active_nonces = []
+        self.hash = {}
+        self.realm = realm
+        self.load_htdigest(htdigest, realm)
+
+    def load_htdigest(self, filename, realm):
+        """Load account information from apache style htdigest files, only
+        users from the specified realm are used
+        """
+        fd = open(filename, 'r')
+        for line in fd.readlines():
+            u, r, a1 = line.strip().split(':')
+            if r == realm:
+                self.hash[u] = a1
+        if self.hash == {}:
+            print >> sys.stderr, "Warning: found no users in realm:", realm
+        
+    def parse_auth_header(self, authorization):
+        values = {}
+        for value in urllib2.parse_http_list(authorization):
+            n, v = value.split('=', 1)
+            if v[0] == '"' and v[-1] == '"':
+                values[n] = v[1:-1]
+            else:
+                values[n] = v
+        return values
+
+    def send_auth_request(self, environ, start_response, stale='false'):
+        """Send a digest challange to the browser. Record used nonces
+        to avoid replay attacks.
+        """
+        nonce = hex_entropy()
+        self.active_nonces.append(nonce)
+        if len(self.active_nonces) > self.MAX_NONCES:
+            self.active_nonces = self.active_nonces[-self.MAX_NONCES:]
+        start_response('401 Unauthorized',
+                       [('WWW-Authenticate',
+                        'Digest realm="%s", nonce="%s", qop="auth", stale="%s"'
+                        % (self.realm, nonce, stale))])('')
+
+    def do_auth(self, environ, start_response):
+        header = environ.get('HTTP_AUTHORIZATION')
+        if not header or not header.startswith('Digest'):
+            self.send_auth_request(environ, start_response)
+            return None
+
+        auth = self.parse_auth_header(header[7:])
+        required_keys = ['username', 'realm', 'nonce', 'uri', 'response',
+                         'nc', 'cnonce']
+        # Invalid response?
+        for key in required_keys:
+            if not auth.has_key(key):
+                self.send_auth_request(environ, start_response)
+                return None
+        # Unknown user?
+        if not self.hash.has_key(auth['username']):
+            self.send_auth_request(environ, start_response)
+            return None
+
+        kd = lambda x: md5.md5(':'.join(x)).hexdigest()
+        a1 = self.hash[auth['username']]
+        a2 = kd([environ['REQUEST_METHOD'], auth['uri']])
+        # Is the response correct?
+        correct = kd([a1, auth['nonce'], auth['nc'],
+                      auth['cnonce'], auth['qop'], a2])
+        if auth['response'] != correct:
+            self.send_auth_request(environ, start_response)
+            return None
+        # Is the nonce active, if not ask the client to use a new one
+        if not auth['nonce'] in self.active_nonces:
+            self.send_auth_request(environ, start_response, stale='true')
+            return None
+        self.active_nonces.remove(auth['nonce'])
+        return auth['username']
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/cgi_frontend.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005 Matthew Good <trac@matt-good.net>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+#         Matthew Good <trac@matt-good.net>
+
+import os
+import sys
+
+from trac.web.main import dispatch_request
+from trac.web.wsgi import WSGIGateway
+
+
+class CGIGateway(WSGIGateway):
+
+    wsgi_multithread = False
+    wsgi_multiprocess = False
+    wsgi_run_once = True
+
+    def __init__(self):
+        WSGIGateway.__init__(self, dict(os.environ))
+
+    def _write(self, data):
+        assert self.headers_set, 'Response not started'
+
+        if not self.headers_sent:
+            status, headers = self.headers_sent = self.headers_set
+            sys.stdout.write('Status: %s\r\n' % status)
+            for header in headers:
+                sys.stdout.write('%s: %s\r\n' % header)
+            sys.stdout.write('\r\n')
+            sys.stdout.flush()
+
+        sys.stdout.write(data)
+        sys.stdout.flush()
+
+
+def run():
+    try: # Make FreeBSD use blocking I/O like other platforms
+        import fcntl
+        for stream in [sys.stdin, sys.stdout]:
+            fd = stream.fileno()
+            flags = fcntl.fcntl(fd, fcntl.F_GETFL)
+            fcntl.fcntl(fd, fcntl.F_SETFL, flags & ~os.O_NONBLOCK)
+    except ImportError:
+        pass
+
+    try: # Use binary I/O on Windows
+        import msvcrt
+        msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY)
+        msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
+    except ImportError:
+        pass
+
+    gateway = CGIGateway()
+    gateway.run(dispatch_request)
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/chrome.py
@@ -0,0 +1,337 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+import os
+import re
+
+from trac import mimeview
+from trac.config import *
+from trac.core import *
+from trac.env import IEnvironmentSetupParticipant
+from trac.util.markup import html
+from trac.web.api import IRequestHandler, HTTPNotFound
+from trac.web.href import Href
+from trac.wiki import IWikiSyntaxProvider
+
+def add_link(req, rel, href, title=None, mimetype=None, classname=None):
+    """Add a link to the HDF data set that will be inserted as <link> element in
+    the <head> of the generated HTML
+    """
+    link = {'href': href}
+    if title:
+        link['title'] = title
+    if mimetype:
+        link['type'] = mimetype
+    if classname:
+        link['class'] = classname
+    idx = 0
+    while req.hdf.get('chrome.links.%s.%d.href' % (rel, idx)):
+        idx += 1
+    req.hdf['chrome.links.%s.%d' % (rel, idx)] = link
+
+def add_stylesheet(req, filename, mimetype='text/css'):
+    """Add a link to a style sheet to the HDF data set so that it gets included
+    in the generated HTML page.
+    """
+    if filename.startswith('common/') and 'htdocs_location' in req.hdf:
+        href = Href(req.hdf['htdocs_location'])
+        filename = filename[7:]
+    else:
+        href = Href(req.base_path).chrome
+    add_link(req, 'stylesheet', href(filename), mimetype=mimetype)
+
+def add_script(req, filename, mimetype='text/javascript'):
+    """Add a reference to an external javascript file to the template."""
+    if filename.startswith('common/') and 'htdocs_location' in req.hdf:
+        href = Href(req.hdf['htdocs_location'])
+        filename = filename[7:]
+    else:
+        href = Href(req.base_path).chrome
+    href = href(filename)
+    idx = 0
+    while True:
+        js = req.hdf.get('chrome.scripts.%i.href' % idx)
+        if not js:
+            break
+        if js == href: # already added
+            return
+        idx += 1
+    req.hdf['chrome.scripts.%i' % idx] = {'href': href, 'type': mimetype}
+
+def add_javascript(req, filename):
+    """Deprecated: use `add_script()` instead."""
+    add_script(req, filename, mimetype='text/javascript')
+
+
+class INavigationContributor(Interface):
+    """Extension point interface for components that contribute items to the
+    navigation.
+    """
+
+    def get_active_navigation_item(req):
+        """This method is only called for the `IRequestHandler` processing the
+        request.
+        
+        It should return the name of the navigation item that should be
+        highlighted as active/current.
+        """
+
+    def get_navigation_items(req):
+        """Should return an iterable object over the list of navigation items to
+        add, each being a tuple in the form (category, name, text).
+        """
+
+
+class ITemplateProvider(Interface):
+    """Extension point interface for components that provide their own
+    ClearSilver templates and accompanying static resources.
+    """
+
+    def get_htdocs_dirs():
+        """Return a list of directories with static resources (such as style
+        sheets, images, etc.)
+
+        Each item in the list must be a `(prefix, abspath)` tuple. The
+        `prefix` part defines the path in the URL that requests to these
+        resources are prefixed with.
+        
+        The `abspath` is the absolute path to the directory containing the
+        resources on the local file system.
+        """
+
+    def get_templates_dirs():
+        """Return a list of directories containing the provided ClearSilver
+        templates.
+        """
+
+
+class Chrome(Component):
+    """Responsible for assembling the web site chrome, i.e. everything that
+    is not actual page content.
+    """
+    implements(IEnvironmentSetupParticipant, IRequestHandler, ITemplateProvider,
+               IWikiSyntaxProvider)
+
+    navigation_contributors = ExtensionPoint(INavigationContributor)
+    template_providers = ExtensionPoint(ITemplateProvider)
+
+    templates_dir = Option('trac', 'templates_dir', default_dir('templates'),
+        """Path to the !ClearSilver templates.""")
+
+    htdocs_location = Option('trac', 'htdocs_location', '',
+        """Base URL of the core static resources.""")
+
+    metanav_order = ListOption('trac', 'metanav',
+                               'login,logout,settings,help,about', doc=
+        """List of items IDs to display in the navigation bar `metanav`.""")
+
+    mainnav_order = ListOption('trac', 'mainnav',
+                               'wiki,timeline,roadmap,browser,tickets,'
+                               'newticket,search', doc=
+        """List of item IDs to display in the navigation bar `mainnav`.""")
+
+    logo_link = Option('header_logo', 'link', 'http://example.org/',
+        """URL to link to from header logo.""")
+
+    logo_src = Option('header_logo', 'src', 'common/trac_banner.png',
+        """URL of the image to use as header logo.""")
+
+    logo_alt = Option('header_logo', 'alt', '',
+        """Alternative text for the header logo.""")
+
+    logo_width = IntOption('header_logo', 'width', -1,
+        """Width of the header logo image in pixels.""")
+
+    logo_height = IntOption('header_logo', 'height', -1,
+        """Height of the header logo image in pixels.""")
+
+    # IEnvironmentSetupParticipant methods
+
+    def environment_created(self):
+        """Create the templates directory and some templates for
+        customization.
+        """
+        def _create_file(filename, data=None):
+            fd = open(filename, 'w')
+            if data:
+                fd.write(data)
+            fd.close()
+
+        if self.env.path:
+            templates_dir = os.path.join(self.env.path, 'templates')
+            if not os.path.exists(templates_dir):
+                os.mkdir(templates_dir)
+            _create_file(os.path.join(templates_dir, 'README'),
+                        'This directory contains project-specific custom '
+                        'templates and style sheet.\n')
+            _create_file(os.path.join(templates_dir, 'site_header.cs'),
+                         """<?cs
+####################################################################
+# Site header - Contents are automatically inserted above Trac HTML
+?>
+""")
+            _create_file(os.path.join(templates_dir, 'site_footer.cs'),
+                         """<?cs
+#########################################################################
+# Site footer - Contents are automatically inserted after main Trac HTML
+?>
+""")
+            _create_file(os.path.join(templates_dir, 'site_css.cs'),
+                         """<?cs
+##################################################################
+# Site CSS - Place custom CSS, including overriding styles here.
+?>
+""")
+
+    def environment_needs_upgrade(self, db):
+        return False
+
+    def upgrade_environment(self, db):
+        pass
+
+
+    # IRequestHandler methods
+
+    anonymous_request = True
+    use_template = False
+
+    def match_request(self, req):
+        match = re.match(r'/chrome/(?P<prefix>[^/]+)/(?P<filename>[/\w\-\.]+)',
+                         req.path_info)
+        if match:
+            req.args['prefix'] = match.group('prefix')
+            req.args['filename'] = match.group('filename')
+            return True
+
+    def process_request(self, req):
+        prefix = req.args['prefix']
+        filename = req.args['filename']
+
+        dirs = []
+        for provider in self.template_providers:
+            for dir in [os.path.normpath(dir[1]) for dir
+                        in provider.get_htdocs_dirs() if dir[0] == prefix]:
+                dirs.append(dir)
+                path = os.path.normpath(os.path.join(dir, filename))
+                assert os.path.commonprefix([dir, path]) == dir
+                if os.path.isfile(path):
+                    req.send_file(path)
+
+        self.log.warning('File %s not found in any of %s', filename, dirs)
+        raise HTTPNotFound('File %s not found', filename)
+
+    # ITemplateProvider methods
+
+    def get_htdocs_dirs(self):
+        from trac.config import default_dir
+        return [('common', default_dir('htdocs')),
+                ('site', self.env.get_htdocs_dir())]
+
+    def get_templates_dirs(self):
+        return [self.env.get_templates_dir(), self.templates_dir]
+
+    # IWikiSyntaxProvider methods
+    
+    def get_wiki_syntax(self):
+        return []
+    
+    def get_link_resolvers(self):
+        yield ('htdocs', self._format_link)
+
+    def _format_link(self, formatter, ns, file, label):
+        return html.A(label, href=formatter.href.chrome('site', file))
+
+    # Public API methods
+
+    def get_all_templates_dirs(self):
+        """Return a list of the names of all known templates directories."""
+        dirs = []
+        for provider in self.template_providers:
+            dirs += provider.get_templates_dirs()
+        return dirs
+
+    def populate_hdf(self, req, handler):
+        """Add chrome-related data to the HDF."""
+
+        # Provided for template customization
+        req.hdf['HTTP.PathInfo'] = req.path_info
+
+        href = Href(req.base_path)
+        req.hdf['chrome.href'] = href.chrome()
+        htdocs_location = self.htdocs_location or href.chrome('common')
+        req.hdf['htdocs_location'] = htdocs_location.rstrip('/') + '/'
+
+        # HTML <head> links
+        add_link(req, 'start', req.href.wiki())
+        add_link(req, 'search', req.href.search())
+        add_link(req, 'help', req.href.wiki('TracGuide'))
+        add_stylesheet(req, 'common/css/trac.css')
+        add_script(req, 'common/js/trac.js')
+
+        icon = self.env.project_icon
+        if icon:
+            if not icon.startswith('/') and icon.find('://') == -1:
+                if '/' in icon:
+                    icon = href.chrome(icon)
+                else:
+                    icon = href.chrome('common', icon)
+            mimetype = mimeview.get_mimetype(icon)
+            add_link(req, 'icon', icon, mimetype=mimetype)
+            add_link(req, 'shortcut icon', icon, mimetype=mimetype)
+
+        # Logo image
+        logo_src = self.logo_src
+        if logo_src:
+            logo_src_abs = logo_src.startswith('http://') or \
+                           logo_src.startswith('https://')
+            if not logo_src.startswith('/') and not logo_src_abs:
+                if '/' in logo_src:
+                    logo_src = href.chrome(logo_src)
+                else:
+                    logo_src = href.chrome('common', logo_src)
+            width = self.logo_width > -1 and self.logo_width
+            height = self.logo_height > -1 and self.logo_height
+            req.hdf['chrome.logo'] = {
+                'link': self.logo_link, 'src': logo_src,
+                'src_abs': logo_src_abs, 'alt': self.logo_alt,
+                'width': width, 'height': height
+            }
+        else:
+            req.hdf['chrome.logo.link'] = self.logo_link
+
+        # Navigation links
+        navigation = {}
+        active = None
+        for contributor in self.navigation_contributors:
+            for category, name, text in contributor.get_navigation_items(req):
+                navigation.setdefault(category, {})[name] = text
+            if contributor is handler:
+                active = contributor.get_active_navigation_item(req)
+
+        for category, items in [(k, v.items()) for k, v in navigation.items()]:
+            order = getattr(self, category + '_order')
+            def navcmp(x, y):
+                if x[0] not in order:
+                    return int(y[0] in order)
+                if y[0] not in order:
+                    return -int(x[0] in order)
+                return cmp(order.index(x[0]), order.index(y[0]))
+            items.sort(navcmp)
+
+            for name, text in items:
+                req.hdf['chrome.nav.%s.%s' % (category, name)] = text
+                if name == active:
+                    req.hdf['chrome.nav.%s.%s.active' % (category, name)] = 1
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/clearsilver.py
@@ -0,0 +1,294 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+from trac.core import TracError
+from trac.util.markup import Markup, Fragment, escape
+from trac.util.text import to_unicode
+
+
+class HDFWrapper:
+    """
+    Convenience layer on top of the low-level ClearSilver python bindings
+    for HDF manipulation. This class makes the HDF look and behave more
+    like a standard Python dict.
+
+    >>> hdf = HDFWrapper()
+    >>> hdf['trac.url'] = 'http://projects.edgewall.com/trac/'
+    >>> hdf['trac.version'] = '1.0'
+    >>> print hdf
+    trac {
+      url = http://projects.edgewall.com/trac/
+      version = 1.0
+    }
+
+    HDFWrapper can also assign Python lists and dicts to HDF nodes,
+    automatically expanding them into the corresponding HDF structure.
+
+    A dictionary is mapped to a HDF node with named children:
+
+    >>> hdf = HDFWrapper()
+    >>> hdf['item'] = {'name': 'An item', 'value': '0'}
+    >>> print hdf
+    item {
+      name = An item
+      value = 0
+    }
+
+    A sequence is mapped to a HDF node with children whose names are
+    the indexes of the elements:
+
+    >>> hdf = HDFWrapper()
+    >>> hdf['items'] = ['Item 1', 'Item 2']
+    >>> print hdf
+    items {
+      0 = Item 1
+      1 = Item 2
+    }
+
+    Simple values can also be easily retrieved using the same syntax.
+
+    >>> hdf = HDFWrapper()
+    >>> hdf['time'] = 42
+    >>> hdf['time']
+    u'42'
+    >>> hdf['name'] = 'Foo'
+    >>> hdf['name']
+    u'Foo'
+
+    An attempt to retrieve a value that hasn't been set will raise a KeyError,
+    just like a standard dictionary:
+
+    >>> hdf['undef']
+    Traceback (most recent call last):
+        ...
+    KeyError: 'undef'
+    
+    It may be preferable to return a default value if the given key does not exit.
+    It will return 'None' when the specified key is not present:
+
+    >>> hdf.get('time')
+    u'42'
+    >>> hdf.get('undef')
+
+    A second argument may be passed to specify the default return value:
+
+    >>> hdf.get('time', 'Undefined Key')
+    u'42'
+    >>> hdf.get('undef', 'Undefined Key')
+    'Undefined Key'
+
+    The 'in' and 'not in' operators can be used to test whether the HDF contains
+    a value with a given name.
+
+    >>> 'name' in hdf
+    True
+    >>> 'undef' in hdf
+    False
+
+    has_key() performs the same function:
+
+    >>> hdf.has_key('name')
+    True
+    >>> hdf.has_key('undef')
+    False
+    """
+
+    def __init__(self, loadpaths=[]):
+        """Create a new HDF dataset.
+        
+        The loadpaths parameter can be used to specify a sequence of paths under
+        which ClearSilver will search for template files:
+
+        >>> hdf = HDFWrapper(loadpaths=['/etc/templates',
+        ...                             '/home/john/templates'])
+        >>> print hdf
+        hdf {
+          loadpaths {
+            0 = /etc/templates
+            1 = /home/john/templates
+          }
+        }
+        """
+        try:
+            import neo_cgi
+            # The following line is needed so that ClearSilver can be loaded when
+            # we are being run in multiple interpreters under mod_python
+            neo_cgi.update()
+            import neo_util
+            self.hdf = neo_util.HDF()
+        except ImportError, e:
+            raise TracError, "ClearSilver not installed (%s)" % e
+        
+        self['hdf.loadpaths'] = loadpaths
+
+    def __getattr__(self, name):
+        # For backwards compatibility, expose the interface of the underlying HDF
+        # object
+        return getattr(self.hdf, name)
+
+    def __contains__(self, name):
+        return self.hdf.getObj(str(name)) != None
+    has_key = __contains__
+
+    def get(self, name, default=None):
+        value = self.hdf.getValue(str(name), '<<NONE>>')
+        if value == '<<NONE>>':
+            return default
+        return value.decode('utf-8')
+
+    def __getitem__(self, name):
+        value = self.get(name, None)
+        if value == None:
+            raise KeyError, name
+        return value
+
+    def __setitem__(self, name, value):
+        """Add data to the HDF dataset.
+        
+        The `name` parameter is the path of the node in dotted syntax. The
+        `value` parameter can be a simple value such as a string or number, but
+        also data structures such as dicts and lists.
+
+        >>> hdf = HDFWrapper()
+
+        Adding a simple value results in that value being inserted into the HDF
+        after being converted to a string.
+
+        >>> hdf['test.num'] = 42
+        >>> hdf['test.num']
+        u'42'
+        >>> hdf['test.str'] = 'foo'
+        >>> hdf['test.str']
+        u'foo'
+
+        The boolean literals `True` and `False` are converted to there integer
+        representation before being added:
+
+        >>> hdf['test.true'] = True
+        >>> hdf['test.true']
+        u'1'
+        >>> hdf['test.false'] = False
+        >>> hdf['test.false']
+        u'0'
+
+        If value is `None`, nothing is added to the HDF:
+
+        >>> hdf['test.true'] = None
+        >>> hdf['test.none']
+        Traceback (most recent call last):
+            ...
+        KeyError: 'test.none'
+        """
+        self.set_value(name, value, True)
+        
+    def set_unescaped(self, name, value):
+        """
+        Add data to the HDF dataset.
+        
+        This method works the same way as `__setitem__` except that `value`
+        is not escaped if it is a string.
+        """
+        self.set_value(name, value, False)
+        
+    def set_value(self, name, value, do_escape=True):
+        """
+        Add data to the HDF dataset.
+        """
+        def set_unicode(prefix, value):
+            self.hdf.setValue(prefix.encode('utf-8'), value.encode('utf-8'))
+        def set_str(prefix, value):
+            self.hdf.setValue(prefix.encode('utf-8'), str(value))
+            
+        def add_value(prefix, value):
+            if value is None:
+                return
+            if value in (True, False):
+                set_str(prefix, int(value))
+            elif isinstance(value, (Markup, Fragment)):
+                set_unicode(prefix, unicode(value))
+            elif isinstance(value, str):
+                if do_escape:
+                    # Assume UTF-8 here, for backward compatibility reasons
+                    set_unicode(prefix, escape(to_unicode(value)))
+                else:
+                    set_str(prefix, value)
+            elif isinstance(value, unicode):
+                if do_escape:
+                    set_unicode(prefix, escape(value))
+                else:
+                    set_unicode(prefix, value)
+            elif isinstance(value, dict):
+                for k in value.keys():
+                    add_value('%s.%s' % (prefix, k), value[k])
+            else:
+                if hasattr(value, '__iter__') or \
+                        isinstance(value, (list, tuple)):
+                    for idx, item in enumerate(value):
+                        add_value('%s.%d' % (prefix, idx), item)
+                else:
+                    set_str(prefix, value)
+        add_value(name, value)
+
+    def __str__(self):
+        from StringIO import StringIO
+        buf = StringIO()
+        def hdf_tree_walk(node, prefix=''):
+            while node:
+                name = node.name() or ''
+                buf.write('%s%s' % (prefix, name))
+                value = node.value()
+                if value or not node.child():
+                    if value.find('\n') == -1:
+                        buf.write(' = %s' % value)
+                    else:
+                        buf.write(' = << EOM\n%s\nEOM' % value)
+                if node.child():
+                    buf.write(' {\n')
+                    hdf_tree_walk(node.child(), prefix + '  ')
+                    buf.write('%s}\n' % prefix)
+                else:
+                    buf.write('\n')
+                node = node.next()
+        hdf_tree_walk(self.hdf.child())
+        return buf.getvalue().strip()
+
+    def parse(self, string):
+        """Parse the given string as template text, and returns a neo_cs.CS
+        object.
+        """
+        import neo_cs
+        cs = neo_cs.CS(self.hdf)
+        cs.parseStr(string)
+        return cs
+
+    def render(self, template):
+        """Render the HDF using the given template.
+
+        The template parameter can be either an already parse neo_cs.CS
+        object, or a string. In the latter case it is interpreted as name of the
+        template file.
+        """
+        if isinstance(template, basestring):
+            filename = template
+            import neo_cs
+            template = neo_cs.CS(self.hdf)
+            template.parseFile(filename)
+        return template.render()
+
+
+if __name__ == '__main__':
+    import doctest, sys
+    doctest.testmod(sys.modules[__name__])
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/fcgi_frontend.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Matthew Good <trac@matt-good.net>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Matthew Good <trac@matt-good.net>
+
+from trac.web.main import dispatch_request
+
+import _fcgi
+
+def run():
+    _fcgi.WSGIServer(dispatch_request).run()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/href.py
@@ -0,0 +1,164 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2004 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+from urllib import quote, urlencode
+from trac.util.text import unicode_quote, unicode_urlencode
+
+
+class Href(object):
+    """
+    Implements a callable that constructs URLs with the given base. The
+    function can be called with any number of positional and keyword
+    arguments which than are used to assemble the URL.
+
+    Positional arguments are appended as individual segments to
+    the path of the URL:
+
+    >>> href = Href('/trac')
+    >>> href('ticket', 540)
+    '/trac/ticket/540'
+    >>> href('ticket', 540, 'attachment', 'bugfix.patch')
+    '/trac/ticket/540/attachment/bugfix.patch'
+    >>> href('ticket', '540/attachment/bugfix.patch')
+    '/trac/ticket/540/attachment/bugfix.patch'
+
+    If a positional parameter evaluates to None, it will be skipped:
+
+    >>> href('ticket', 540, 'attachment', None)
+    '/trac/ticket/540/attachment'
+
+    The first path segment can also be specified by calling an attribute
+    of the instance, as follows:
+
+    >>> href.ticket(540)
+    '/trac/ticket/540'
+    >>> href.changeset(42, format='diff')
+    '/trac/changeset/42?format=diff'
+
+    Simply calling the Href object with no arguments will return the base URL:
+
+    >>> href()
+    '/trac'
+
+    Keyword arguments are added to the query string, unless the value is None:
+
+    >>> href = Href('/trac')
+    >>> href('timeline', format='rss')
+    '/trac/timeline?format=rss'
+    >>> href('timeline', format=None)
+    '/trac/timeline'
+    >>> href('search', q='foo bar')
+    '/trac/search?q=foo+bar'
+
+    Multiple values for one parameter are specified using a sequence (a list or
+    tuple) for the parameter:
+
+    >>> href('timeline', show=['ticket', 'wiki', 'changeset'])
+    '/trac/timeline?show=ticket&show=wiki&show=changeset'
+
+    Alternatively, query string parameters can be added by passing a dict or
+    list as last positional argument:
+
+    >>> href('timeline', {'from': '02/24/05', 'daysback': 30})
+    '/trac/timeline?daysback=30&from=02%2F24%2F05'
+
+    If the order of query string parameters should be preserved, you may also
+    pass a sequence of (name, value) tuples as last positional argument:
+
+    >>> href('query', (('group', 'component'), ('groupdesc', 1)))
+    '/trac/query?group=component&groupdesc=1'
+
+    >>> params = []
+    >>> params.append(('group', 'component'))
+    >>> params.append(('groupdesc', 1))
+    >>> href('query', params)
+    '/trac/query?group=component&groupdesc=1'
+
+    By specifying an absolute base, the function returned will also generate
+    absolute URLs:
+
+    >>> href = Href('http://projects.edgewall.com/trac')
+    >>> href('ticket', 540)
+    'http://projects.edgewall.com/trac/ticket/540'
+
+    >>> href = Href('https://projects.edgewall.com/trac')
+    >>> href('ticket', 540)
+    'https://projects.edgewall.com/trac/ticket/540'
+
+    In common usage, it may improve readability to use the function-calling
+    ability for the first component of the URL as mentioned earlier:
+
+    >>> href = Href('/trac')
+    >>> href.ticket(540)
+    '/trac/ticket/540'
+    >>> href.browser('/trunk/README.txt', format='txt')
+    '/trac/browser/trunk/README.txt?format=txt'
+    """
+
+    def __init__(self, base):
+        self.base = base
+        self._derived = {}
+
+    def __call__(self, *args, **kw):
+        href = self.base
+        if href and href[-1] == '/':
+            href = href[:-1]
+        params = []
+
+        def add_param(name, value):
+            if type(value) in (list, tuple):
+                for i in [i for i in value if i != None]:
+                    params.append((name, i))
+            elif v != None:
+                params.append((name, value))
+
+        if args:
+            lastp = args[-1]
+            if lastp and type(lastp) is dict:
+                for k,v in lastp.items():
+                    add_param(k, v)
+                args = args[:-1]
+            elif lastp and type(lastp) in (list, tuple):
+                for k,v in lastp:
+                    add_param(k, v)
+                args = args[:-1]
+
+        # build the path
+        path = '/'.join([unicode_quote(unicode(arg).strip('/')) for arg in args
+                         if arg != None])
+        if path:
+            href += '/' + path
+
+        # assemble the query string
+        for k,v in kw.items():
+            add_param(k, v)
+
+        if params:
+            href += '?' + unicode_urlencode(params)
+
+        return href
+
+    def __getattr__(self, name):
+        if not self._derived.has_key(name):
+            self._derived[name] = lambda *args, **kw: self(name, *args, **kw)
+        return self._derived[name]
+
+
+if __name__ == '__main__':
+    import doctest, sys
+    doctest.testmod(sys.modules[__name__])
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/main.py
@@ -0,0 +1,423 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005 Matthew Good <trac@matt-good.net>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+#         Matthew Good <trac@matt-good.net>
+
+import locale
+import os
+import sys
+import dircache
+import urllib
+
+from trac.config import ExtensionOption, OrderedExtensionsOption
+from trac.core import *
+from trac.env import open_environment
+from trac.perm import PermissionCache, NoPermissionCache, PermissionError
+from trac.util import reversed, get_last_traceback
+from trac.util.datefmt import format_datetime, http_date
+from trac.util.text import to_unicode
+from trac.util.markup import Markup
+from trac.web.api import *
+from trac.web.chrome import Chrome
+from trac.web.clearsilver import HDFWrapper
+from trac.web.href import Href
+from trac.web.session import Session
+
+# Environment cache for multithreaded front-ends:
+try:
+    import threading
+except ImportError:
+    import dummy_threading as threading
+
+env_cache = {}
+env_cache_lock = threading.Lock()
+
+def _open_environment(env_path, run_once=False):
+    if run_once:
+        return open_environment(env_path)
+
+    global env_cache, env_cache_lock
+    env = None
+    env_cache_lock.acquire()
+    try:
+        if not env_path in env_cache:
+            env_cache[env_path] = open_environment(env_path)
+        env = env_cache[env_path]
+    finally:
+        env_cache_lock.release()
+
+    # Re-parse the configuration file if it changed since the last the time it
+    # was parsed
+    env.config.parse_if_needed()
+
+    return env
+
+def populate_hdf(hdf, env, req=None):
+    """Populate the HDF data set with various information, such as common URLs,
+    project information and request-related information.
+    FIXME: do we really have req==None at times?
+    """
+    from trac import __version__
+    hdf['trac'] = {
+        'version': __version__,
+        'time': format_datetime(),
+        'time.gmt': http_date()
+    }
+    hdf['project'] = {
+        'name': env.project_name,
+        'name_encoded': env.project_name,
+        'descr': env.project_description,
+        'footer': Markup(env.project_footer),
+        'url': env.project_url
+    }
+
+    if req:
+        hdf['trac.href'] = {
+            'wiki': req.href.wiki(),
+            'browser': req.href.browser('/'),
+            'timeline': req.href.timeline(),
+            'roadmap': req.href.roadmap(),
+            'milestone': req.href.milestone(None),
+            'report': req.href.report(),
+            'query': req.href.query(),
+            'newticket': req.href.newticket(),
+            'search': req.href.search(),
+            'about': req.href.about(),
+            'about_config': req.href.about('config'),
+            'login': req.href.login(),
+            'logout': req.href.logout(),
+            'settings': req.href.settings(),
+            'homepage': 'http://trac.edgewall.com/'
+        }
+
+        hdf['base_url'] = req.base_url
+        hdf['base_host'] = req.base_url[:req.base_url.rfind(req.base_path)]
+        hdf['cgi_location'] = req.base_path
+        hdf['trac.authname'] = req.authname
+
+        if req.perm:
+            for action in req.perm.permissions():
+                req.hdf['trac.acl.' + action] = True
+
+        for arg in [k for k in req.args.keys() if k]:
+            if isinstance(req.args[arg], (list, tuple)):
+                hdf['args.%s' % arg] = [v for v in req.args[arg]]
+            elif isinstance(req.args[arg], basestring):
+                hdf['args.%s' % arg] = req.args[arg]
+            # others are file uploads
+
+
+class RequestDispatcher(Component):
+    """Component responsible for dispatching requests to registered handlers."""
+
+    authenticators = ExtensionPoint(IAuthenticator)
+    handlers = ExtensionPoint(IRequestHandler)
+
+    filters = OrderedExtensionsOption('trac', 'request_filters', IRequestFilter,
+        doc="""Ordered list of filters to apply to all requests
+            (''since 0.10'').""")
+
+    default_handler = ExtensionOption('trac', 'default_handler',
+                                      IRequestHandler, 'WikiModule',
+        """Name of the component that handles requests to the base URL.
+        
+        Options include `TimeLineModule`, `RoadmapModule`, `BrowserModule`,
+        `QueryModule`, `ReportModule` and `NewticketModule` (''since 0.9'').""")
+
+    # Public API
+
+    def authenticate(self, req):
+        for authenticator in self.authenticators:
+            authname = authenticator.authenticate(req)
+            if authname:
+                return authname
+        else:
+            return 'anonymous'
+
+    def dispatch(self, req):
+        """Find a registered handler that matches the request and let it process
+        it.
+        
+        In addition, this method initializes the HDF data set and adds the web
+        site chrome.
+        """
+        # FIXME: For backwards compatibility, should be removed in 0.11
+        self.env.href = req.href
+        self.env.abs_href = req.abs_href
+
+        # Select the component that should handle the request
+        chosen_handler = None
+        if not req.path_info or req.path_info == '/':
+            chosen_handler = self.default_handler
+        else:
+            for handler in self.handlers:
+                if handler.match_request(req):
+                    chosen_handler = handler
+                    break
+
+        for filter_ in self.filters:
+            chosen_handler = filter_.pre_process_request(req, chosen_handler)
+
+        if not chosen_handler:
+            raise HTTPNotFound('No handler matched request to %s',
+                               req.path_info)
+
+        # Attach user information to the request
+        anonymous_request = getattr(chosen_handler, 'anonymous_request', False)
+        if anonymous_request:
+            req.authname = 'anonymous'
+            req.perm = NoPermissionCache()
+        else:
+            req.authname = self.authenticate(req)
+            req.perm = PermissionCache(self.env, req.authname)
+            req.session = Session(self.env, req)
+
+        # Prepare HDF for the clearsilver template
+        use_template = getattr(chosen_handler, 'use_template', True)
+        if use_template:
+            chrome = Chrome(self.env)
+            req.hdf = HDFWrapper(loadpaths=chrome.get_all_templates_dirs())
+            populate_hdf(req.hdf, self.env, req)
+            chrome.populate_hdf(req, chosen_handler)
+
+        # Process the request and render the template
+        try:
+            try:
+                resp = chosen_handler.process_request(req)
+                if resp:
+                    for filter_ in reversed(self.filters):
+                        resp = filter_.post_process_request(req, *resp)
+                    template, content_type = resp
+                    req.display(template, content_type or 'text/html')
+                else:
+                    for filter_ in reversed(self.filters):
+                        filter_.post_process_request(req, None, None)
+            except PermissionError, e:
+                raise HTTPForbidden(to_unicode(e))
+            except TracError, e:
+                raise HTTPInternalError(e.message)
+        finally:
+            # Give the session a chance to persist changes
+            if req.session:
+                req.session.save()
+
+
+def dispatch_request(environ, start_response):
+    """Main entry point for the Trac web interface.
+    
+    @param environ: the WSGI environment dict
+    @param start_response: the WSGI callback for starting the response
+    """
+    if 'mod_python.options' in environ:
+        options = environ['mod_python.options']
+        environ.setdefault('trac.env_path', options.get('TracEnv'))
+        environ.setdefault('trac.env_parent_dir',
+                           options.get('TracEnvParentDir'))
+        environ.setdefault('trac.env_index_template',
+                           options.get('TracEnvIndexTemplate'))
+        environ.setdefault('trac.template_vars',
+                           options.get('TracTemplateVars'))
+        environ.setdefault('trac.locale', options.get('TracLocale'))
+
+        if 'TracUriRoot' in options:
+            # Special handling of SCRIPT_NAME/PATH_INFO for mod_python, which
+            # tends to get confused for whatever reason
+            root_uri = options['TracUriRoot'].rstrip('/')
+            request_uri = environ['REQUEST_URI'].split('?', 1)[0]
+            if not request_uri.startswith(root_uri):
+                raise ValueError('TracUriRoot set to %s but request URL '
+                                 'is %s' % (root_uri, request_uri))
+            environ['SCRIPT_NAME'] = root_uri
+            environ['PATH_INFO'] = urllib.unquote(request_uri[len(root_uri):])
+
+    else:
+        environ.setdefault('trac.env_path', os.getenv('TRAC_ENV'))
+        environ.setdefault('trac.env_parent_dir',
+                           os.getenv('TRAC_ENV_PARENT_DIR'))
+        environ.setdefault('trac.env_index_template',
+                           os.getenv('TRAC_ENV_INDEX_TEMPLATE'))
+        environ.setdefault('trac.template_vars',
+                           os.getenv('TRAC_TEMPLATE_VARS'))
+        environ.setdefault('trac.locale', '')
+
+    locale.setlocale(locale.LC_ALL, environ['trac.locale'])
+
+    # Allow specifying the python eggs cache directory using SetEnv
+    if 'mod_python.subprocess_env' in environ:
+        egg_cache = environ['mod_python.subprocess_env'].get('PYTHON_EGG_CACHE')
+        if egg_cache:
+            os.environ['PYTHON_EGG_CACHE'] = egg_cache
+
+    # Determine the environment
+    env_path = environ.get('trac.env_path')
+    if not env_path:
+        env_parent_dir = environ.get('trac.env_parent_dir')
+        env_paths = environ.get('trac.env_paths')
+        if env_parent_dir or env_paths:
+            # The first component of the path is the base name of the
+            # environment
+            path_info = environ.get('PATH_INFO', '').lstrip('/').split('/')
+            env_name = path_info.pop(0)
+
+            if not env_name:
+                # No specific environment requested, so render an environment
+                # index page
+                send_project_index(environ, start_response, env_parent_dir,
+                                   env_paths)
+                return []
+
+            # To make the matching patterns of request handlers work, we append
+            # the environment name to the `SCRIPT_NAME` variable, and keep only
+            # the remaining path in the `PATH_INFO` variable.
+            environ['SCRIPT_NAME'] = Href(environ['SCRIPT_NAME'])(env_name)
+            environ['PATH_INFO'] = '/'.join([''] + path_info)
+
+            if env_parent_dir:
+                env_path = os.path.join(env_parent_dir, env_name)
+            else:
+                env_path = get_environments(environ).get(env_name)
+
+            if not env_path or not os.path.isdir(env_path):
+                start_response('404 Not Found', [])
+                return ['Environment not found']
+
+    if not env_path:
+        raise EnvironmentError('The environment options "TRAC_ENV" or '
+                               '"TRAC_ENV_PARENT_DIR" or the mod_python '
+                               'options "TracEnv" or "TracEnvParentDir" are '
+                               'missing. Trac requires one of these options '
+                               'to locate the Trac environment(s).')
+    env = _open_environment(env_path, run_once=environ['wsgi.run_once'])
+
+    if env.base_url:
+        environ['trac.base_url'] = env.base_url
+
+    req = Request(environ, start_response)
+    try:
+        db = env.get_db_cnx()
+        try:
+            try:
+                dispatcher = RequestDispatcher(env)
+                dispatcher.dispatch(req)
+            except RequestDone:
+                pass
+            return req._response or []
+        finally:
+            db.close()
+
+    except HTTPException, e:
+        env.log.warn(e)
+        if req.hdf:
+            req.hdf['title'] = e.reason or 'Error'
+            req.hdf['error'] = {
+                'title': e.reason or 'Error',
+                'type': 'TracError',
+                'message': e.message
+            }
+        try:
+            req.send_error(sys.exc_info(), status=e.code)
+        except RequestDone:
+            return []
+
+    except Exception, e:
+        env.log.exception(e)
+
+        if req.hdf:
+            req.hdf['title'] = to_unicode(e) or 'Error'
+            req.hdf['error'] = {
+                'title': to_unicode(e) or 'Error',
+                'type': 'internal',
+                'traceback': get_last_traceback()
+            }
+        try:
+            req.send_error(sys.exc_info(), status=500)
+        except RequestDone:
+            return []
+
+def send_project_index(environ, start_response, parent_dir=None,
+                       env_paths=None):
+    from trac.config import default_dir
+
+    req = Request(environ, start_response)
+
+    loadpaths = [default_dir('templates')]
+    if req.environ.get('trac.env_index_template'):
+        tmpl_path, template = os.path.split(req.environ['trac.env_index_template'])
+        loadpaths.insert(0, tmpl_path)
+    else:
+        template = 'index.cs'
+    req.hdf = HDFWrapper(loadpaths)
+
+    tmpl_vars = {}
+    if req.environ.get('trac.template_vars'):
+        for pair in req.environ['trac.template_vars'].split(','):
+            key, val = pair.split('=')
+            req.hdf[key] = val
+
+    if parent_dir and not env_paths:
+        env_paths = dict([(filename, os.path.join(parent_dir, filename))
+                          for filename in os.listdir(parent_dir)])
+
+    try:
+        href = Href(req.base_path)
+        projects = []
+        for env_name, env_path in get_environments(environ).items():
+            try:
+                env = _open_environment(env_path,
+                                        run_once=environ['wsgi.run_once'])
+                proj = {
+                    'name': env.project_name,
+                    'description': env.project_description,
+                    'href': href(env_name)
+                }
+            except Exception, e:
+                proj = {'name': env_name, 'description': to_unicode(e)}
+            projects.append(proj)
+        projects.sort(lambda x, y: cmp(x['name'].lower(), y['name'].lower()))
+
+        req.hdf['projects'] = projects
+        req.display(template)
+    except RequestDone:
+        pass
+
+def get_environments(environ, warn=False):
+    """Retrieve canonical environment name to path mapping.
+
+    The environments may not be all valid environments, but they are good
+    candidates.
+    """
+    env_paths = environ.get('trac.env_paths', [])
+    env_parent_dir = environ.get('trac.env_parent_dir')
+    if env_parent_dir:
+        env_parent_dir = os.path.normpath(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] == '/']
+    envs = {}
+    for env_path in env_paths:
+        env_path = os.path.normpath(env_path)
+        if not os.path.isdir(env_path):
+            continue
+        env_name = os.path.split(env_path)[1]
+        if env_name in envs:
+            if warn:
+                print >> sys.stderr, ('Warning: Ignoring project "%s" since '
+                                      'it conflicts with project "%s"'
+                                      % (env_path, envs[env_name]))
+        else:
+            envs[env_name] = env_path
+    return envs
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/modpython_frontend.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2005 Edgewall Software
+# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005 Matthew Good <trac@matt-good.net>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+#         Matthew Good <trac@matt-good.net>
+
+from mod_python import apache
+
+from trac.web.main import dispatch_request
+from trac.web.wsgi import WSGIGateway, _ErrorsWrapper
+
+
+class InputWrapper(object):
+
+    def __init__(self, req):
+        self.req = req
+
+    def close(self):
+        pass
+
+    def read(self, size=-1):
+        return self.req.read(size)
+
+    def readline(self):
+        return self.req.readline()
+
+    def readlines(self, hint=-1):
+        return self.req.readlines(hint)
+
+
+class ModPythonGateway(WSGIGateway):
+
+    wsgi_multithread = apache.mpm_query(apache.AP_MPMQ_IS_THREADED) > 0
+    wsgi_multiprocess = apache.mpm_query(apache.AP_MPMQ_IS_FORKED) > 0
+
+    def __init__(self, req, options):
+        environ = {}
+        environ.update(apache.build_cgi_env(req))
+        environ['mod_python.options'] = options
+        environ['mod_python.subprocess_env'] = req.subprocess_env
+        WSGIGateway.__init__(self, environ, InputWrapper(req),
+                             _ErrorsWrapper(lambda x: req.log_error(x)))
+        self.req = req
+
+    def _send_headers(self):
+        assert self.headers_set, 'Response not started'
+
+        if not self.headers_sent:
+            status, headers = self.headers_sent = self.headers_set
+            self.req.status = int(status[:3])
+            for name, value in headers:
+                if name.lower() == 'content-length':
+                    self.req.set_content_length(int(value))
+                elif name.lower() == 'content-type':
+                    self.req.content_type = value
+                else:
+                    self.req.headers_out.add(name, value)
+
+    def _sendfile(self, fileobj):
+        self._send_headers()
+        self.req.sendfile(fileobj.name)
+
+    def _write(self, data):
+        self._send_headers()
+        try:
+            self.req.write(data)
+        except IOError, e:
+            if 'client closed connection' not in str(e):
+                raise
+
+
+def handler(req):
+    options = req.get_options()
+    gateway = ModPythonGateway(req, options)
+    gateway.run(dispatch_request)
+    return apache.OK
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/session.py
@@ -0,0 +1,210 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2004-2005 Edgewall Software
+# Copyright (C) 2004 Daniel Lundin <daniel@edgewall.com>
+# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2006 Jonas Borgström <jonas@edgewall.com>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Daniel Lundin <daniel@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+import time
+
+from trac.core import TracError
+from trac.util import hex_entropy
+from trac.util.markup import Markup
+
+UPDATE_INTERVAL = 3600*24 # Update session last_visit time stamp after 1 day
+PURGE_AGE = 3600*24*90 # Purge session after 90 days idle
+COOKIE_KEY = 'trac_session'
+
+
+class Session(dict):
+    """Basic session handling and per-session storage."""
+
+    def __init__(self, env, req):
+        dict.__init__(self)
+        self.env = env
+        self.req = req
+        self.sid = None
+        self.last_visit = 0
+        self._new = True
+        self._old = {}
+        if req.authname == 'anonymous':
+            if not req.incookie.has_key(COOKIE_KEY):
+                self.sid = hex_entropy(24)
+                self.bake_cookie()
+            else:
+                sid = req.incookie[COOKIE_KEY].value
+                self.get_session(sid)
+        else:
+            if req.incookie.has_key(COOKIE_KEY):
+                sid = req.incookie[COOKIE_KEY].value
+                self.promote_session(sid)
+            self.get_session(req.authname, authenticated=True)
+
+    def bake_cookie(self, expires=PURGE_AGE):
+        assert self.sid, 'Session ID not set'
+        self.req.outcookie[COOKIE_KEY] = self.sid
+        self.req.outcookie[COOKIE_KEY]['path'] = self.req.base_path
+        self.req.outcookie[COOKIE_KEY]['expires'] = expires
+
+    def get_session(self, sid, authenticated=False):
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        refresh_cookie = False
+
+        if self.sid and sid != self.sid:
+            refresh_cookie = True
+        self.sid = sid
+
+        cursor.execute("SELECT last_visit FROM session "
+                       "WHERE sid=%s AND authenticated=%s",
+                       (sid, int(authenticated)))
+        row = cursor.fetchone()
+        if not row:
+            return
+        self._new = False
+        self.last_visit = int(row[0])
+        if self.last_visit and time.time() - self.last_visit > UPDATE_INTERVAL:
+            refresh_cookie = True
+
+        cursor.execute("SELECT name,value FROM session_attribute "
+                       "WHERE sid=%s and authenticated=%s",
+                       (sid, int(authenticated)))
+        for name, value in cursor:
+            self[name] = value
+        self._old.update(self)
+
+        # Refresh the session cookie if this is the first visit since over a day
+        if not authenticated and refresh_cookie:
+            self.bake_cookie()
+
+    def change_sid(self, new_sid):
+        assert self.req.authname == 'anonymous', \
+               'Cannot change ID of authenticated session'
+        assert new_sid, 'Session ID cannot be empty'
+        if new_sid == self.sid:
+            return
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT sid FROM session WHERE sid=%s", (new_sid,))
+        if cursor.fetchone():
+            raise TracError(Markup('Session "%s" already exists.<br />'
+                                   'Please choose a different session ID.',
+                                   new_sid), 'Error renaming session')
+        self.env.log.debug('Changing session ID %s to %s' % (self.sid, new_sid))
+        cursor.execute("UPDATE session SET sid=%s WHERE sid=%s "
+                       "AND authenticated=0", (new_sid, self.sid))
+        cursor.execute("UPDATE session_attribute SET sid=%s "
+                       "WHERE sid=%s and authenticated=0",
+                       (new_sid, self.sid))
+        db.commit()
+        self.sid = new_sid
+        self.bake_cookie()
+
+    def promote_session(self, sid):
+        """Promotes an anonymous session to an authenticated session, if there
+        is no preexisting session data for that user name.
+        """
+        assert self.req.authname != 'anonymous', \
+               'Cannot promote session of anonymous user'
+
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT authenticated FROM session "
+                       "WHERE sid=%s OR sid=%s ", (sid, self.req.authname))
+        authenticated_flags = [row[0] for row in cursor.fetchall()]
+        
+        if len(authenticated_flags) == 2:
+            # There's already an authenticated session for the user, we
+            # simply delete the anonymous session
+            cursor.execute("DELETE FROM session WHERE sid=%s "
+                           "AND authenticated=0", (sid,))
+            cursor.execute("DELETE FROM session_attribute WHERE sid=%s "
+                           "AND authenticated=0", (sid,))
+        elif len(authenticated_flags) == 1:
+            if not authenticated_flags[0]:
+                # Update the anomymous session records so that the session ID
+                # becomes the user name, and set the authenticated flag.
+                self.env.log.debug('Promoting anonymous session %s to '
+                                   'authenticated session for user %s',
+                                   sid, self.req.authname)
+                cursor.execute("UPDATE session SET sid=%s,authenticated=1 "
+                               "WHERE sid=%s AND authenticated=0",
+                               (self.req.authname, sid))
+                cursor.execute("UPDATE session_attribute "
+                               "SET sid=%s,authenticated=1 WHERE sid=%s",
+                               (self.req.authname, sid))
+        else:
+            # we didn't have an anonymous session for this sid
+            cursor.execute("INSERT INTO session (sid,last_visit,authenticated)"
+                           " VALUES(%s,%s,1)",
+                           (self.req.authname, int(time.time())))
+        self._new = False
+        db.commit()
+
+        self.sid = sid
+        self.bake_cookie(0) # expire the cookie
+
+    def save(self):
+        if not self._old and not self.items():
+            # The session doesn't have associated data, so there's no need to
+            # persist it
+            return
+
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        authenticated = int(self.req.authname != 'anonymous')
+
+        if self._new:
+            self._new = False
+            cursor.execute("INSERT INTO session (sid,last_visit,authenticated)"
+                           " VALUES(%s,%s,%s)",
+                           (self.sid, self.last_visit, authenticated))
+        if self._old.items() != self.items():
+            attrs = [(self.sid, authenticated, k, v) for k, v in self.items()]
+            cursor.execute("DELETE FROM session_attribute WHERE sid=%s",
+                           (self.sid,))
+            self._old = dict(self.items())
+            if attrs:
+                cursor.executemany("INSERT INTO session_attribute "
+                                   "(sid,authenticated,name,value) "
+                                   "VALUES(%s,%s,%s,%s)", attrs)
+            elif not authenticated:
+                # No need to keep around empty unauthenticated sessions
+                cursor.execute("DELETE FROM session "
+                               "WHERE sid=%s AND authenticated=0", (self.sid,))
+                return
+
+        now = int(time.time())
+        # Update the session last visit time if it is over an hour old,
+        # so that session doesn't get purged
+        if now - self.last_visit > UPDATE_INTERVAL:
+            self.last_visit = now
+            self.env.log.info("Refreshing session %s" % self.sid)
+            cursor.execute('UPDATE session SET last_visit=%s '
+                           'WHERE sid=%s AND authenticated=%s',
+                           (self.last_visit, self.sid, authenticated))
+            # Purge expired sessions. We do this only when the session was
+            # changed as to minimize the purging.
+            mintime = now - PURGE_AGE
+            self.env.log.debug('Purging old, expired, sessions.')
+            cursor.execute("DELETE FROM session_attribute "
+                           "WHERE authenticated=0 AND sid "
+                           "IN (SELECT sid FROM session WHERE "
+                           "authenticated=0 AND last_visit < %s)",
+                           (mintime,))
+            cursor.execute("DELETE FROM session WHERE "
+                           "authenticated=0 AND last_visit < %s",
+                           (mintime,))
+        db.commit()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/standalone.py
@@ -0,0 +1,242 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2005-2006 Matthew Good <trac@matt-good.net>
+# Copyright (C) 2005-2006 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Matthew Good <trac@matt-good.net>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+import errno
+import os
+import sys
+from SocketServer import ThreadingMixIn
+
+from trac import __version__ as VERSION
+from trac.util import autoreload, daemon
+from trac.web.auth import BasicAuthentication, DigestAuthentication
+from trac.web.main import dispatch_request
+from trac.web.wsgi import WSGIServer, WSGIRequestHandler
+
+
+class AuthenticationMiddleware(object):
+
+    def __init__(self, application, auths):
+        self.application = application
+        self.auths = auths
+
+    def __call__(self, environ, start_response):
+        path_info = environ.get('PATH_INFO', '')
+        path_parts = filter(None, path_info.split('/'))
+        if len(path_parts) > 1 and path_parts[1] == 'login':
+            env_name = path_parts[0]
+            if env_name:
+                auth = self.auths.get(env_name, self.auths.get('*'))
+                if auth:
+                    remote_user = auth.do_auth(environ, start_response)
+                    if not remote_user:
+                        return []
+                    environ['REMOTE_USER'] = remote_user
+        return self.application(environ, start_response)
+
+
+class BasePathMiddleware(object):
+
+    def __init__(self, application, base_path):
+        self.base_path = '/' + base_path.strip('/')
+        self.application = application
+
+    def __call__(self, environ, start_response):
+        path = environ['SCRIPT_NAME'] + environ.get('PATH_INFO', '')
+        environ['PATH_INFO'] = path[len(self.base_path):]
+        environ['SCRIPT_NAME'] = self.base_path
+        return self.application(environ, start_response)
+
+
+class TracEnvironMiddleware(object):
+
+    def __init__(self, application, env_parent_dir, env_paths):
+        self.application = application
+        self.environ = {}
+        self.environ['trac.env_path'] = None
+        if env_parent_dir:
+            self.environ['trac.env_parent_dir'] = env_parent_dir
+        else:
+            self.environ['trac.env_paths'] = env_paths
+
+    def __call__(self, environ, start_response):
+        for k,v in self.environ.iteritems():
+            environ.setdefault(k, v)
+        return self.application(environ, start_response)
+
+
+class TracHTTPServer(ThreadingMixIn, WSGIServer):
+
+    def __init__(self, server_address, application, env_parent_dir, env_paths):
+        WSGIServer.__init__(self, server_address, application,
+                            request_handler=TracHTTPRequestHandler)
+
+
+class TracHTTPRequestHandler(WSGIRequestHandler):
+
+    server_version = 'tracd/' + VERSION
+
+
+def main():
+    from optparse import OptionParser, OptionValueError
+    parser = OptionParser(usage='usage: %prog [options] [projenv] ...',
+                          version='%%prog %s' % VERSION)
+
+    auths = {}
+    def _auth_callback(option, opt_str, value, parser, cls):
+        info = value.split(',', 3)
+        if len(info) != 3:
+            raise OptionValueError("Incorrect number of parameters for %s"
+                                   % option)
+
+        env_name, filename, realm = info
+        if env_name in auths:
+            print >>sys.stderr, 'Ignoring duplicate authentication option for ' \
+                                'project: %s' % env_name
+        else:
+            auths[env_name] = cls(filename, realm)
+
+    def _validate_callback(option, opt_str, value, parser, valid_values):
+        if value not in valid_values:
+            raise OptionValueError('%s must be one of: %s, not %s'
+                                   % (opt_str, '|'.join(valid_values), value))
+        setattr(parser.values, option.dest, value)
+
+    parser.add_option('-a', '--auth', action='callback', type='string',
+                      metavar='DIGESTAUTH', callback=_auth_callback,
+                      callback_args=(DigestAuthentication,),
+                      help='[projectdir],[htdigest_file],[realm]')
+    parser.add_option('--basic-auth', action='callback', type='string',
+                      metavar='BASICAUTH', callback=_auth_callback,
+                      callback_args=(BasicAuthentication,),
+                      help='[projectdir],[htpasswd_file],[realm]')
+
+    parser.add_option('-p', '--port', action='store', type='int', dest='port',
+                      help='the port number to bind to')
+    parser.add_option('-b', '--hostname', action='store', dest='hostname',
+                      help='the host name or IP address to bind to')
+    parser.add_option('--protocol', action='callback', type="string",
+                      dest='protocol', callback=_validate_callback,
+                      callback_args=(('http', 'scgi', 'ajp'),),
+                      help='http|scgi|ajp')
+    parser.add_option('-e', '--env-parent-dir', action='store',
+                      dest='env_parent_dir', metavar='PARENTDIR',
+                      help='parent directory of the project environments')
+    parser.add_option('--base-path', action='store', type='string', # XXX call this url_base_path?
+                      dest='base_path',
+                      help='base path')
+
+    parser.add_option('-r', '--auto-reload', action='store_true',
+                      dest='autoreload',
+                      help='restart automatically when sources are modified')
+
+    if os.name == 'posix':
+        parser.add_option('-d', '--daemonize', action='store_true',
+                          dest='daemonize',
+                          help='run in the background as a daemon')
+        parser.add_option('--pidfile', action='store',
+                          dest='pidfile',
+                          help='When daemonizing, file to which to write pid')
+
+    parser.set_defaults(port=None, hostname='', base_path='', daemonize=False,
+                        protocol='http')
+    options, args = parser.parse_args()
+
+    if not args and not options.env_parent_dir:
+        parser.error('either the --env-parent-dir option or at least one '
+                     'environment must be specified')
+
+    if options.port is None:
+        options.port = {
+            'http': 80,
+            'scgi': 4000,
+            'ajp': 8009,
+        }[options.protocol]
+    server_address = (options.hostname, options.port)
+
+    wsgi_app = TracEnvironMiddleware(dispatch_request,
+                                     options.env_parent_dir, args)
+    if auths:
+        wsgi_app = AuthenticationMiddleware(wsgi_app, auths)
+    base_path = options.base_path.strip('/')
+    if base_path:
+        wsgi_app = BasePathMiddleware(wsgi_app, base_path)
+
+    if options.protocol == 'http':
+        def serve():
+            httpd = TracHTTPServer(server_address, wsgi_app,
+                                   options.env_parent_dir, args)
+            httpd.serve_forever()
+    elif options.protocol in ('scgi', 'ajp'):
+        def serve():
+            server_cls = __import__('flup.server.%s' % options.protocol,
+                                    None, None, ['']).WSGIServer
+            ret = server_cls(wsgi_app, bindAddress=server_address).run()
+            sys.exit(ret and 42 or 0) # if SIGHUP exit with status 42
+
+    try:
+        if os.name == 'posix':
+            if options.pidfile:
+                options.pidfile = os.path.abspath(options.pidfile)
+                if os.path.exists(options.pidfile):
+                    pidfile = open(options.pidfile)
+                    try:
+                        pid = int(pidfile.read())
+                    finally:
+                        pidfile.close()
+
+                    try:
+                        # signal the process to see if it is still running
+                        os.kill(pid, 0)
+                    except OSError, e:
+                        if e.errno != errno.ESRCH:
+                            raise
+                    else:
+                        sys.exit("tracd is already running with pid %s" % pid)
+                realserve = serve
+                def serve():
+                    try:
+                        pidfile = open(options.pidfile, 'w')
+                        try:
+                            pidfile.write(str(os.getpid()))
+                        finally:
+                            pidfile.close()
+                        realserve()
+                    finally:
+                       if os.path.exists(options.pidfile):
+                           os.remove(options.pidfile)
+
+            if options.daemonize:
+                daemon.daemonize()
+
+        if options.autoreload:
+            def modification_callback(file):
+                print>>sys.stderr, 'Detected modification of %s, restarting.' \
+                                   % file
+            autoreload.main(serve, modification_callback)
+        else:
+            serve()
+
+    except OSError:
+        sys.exit(1)
+    except KeyboardInterrupt:
+        pass
+
+if __name__ == '__main__':
+    main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/tests/__init__.py
@@ -0,0 +1,20 @@
+import unittest
+
+from trac.web.tests import api, auth, cgi_frontend, chrome, clearsilver, \
+                           href, session, wikisyntax
+
+def suite():
+
+    suite = unittest.TestSuite()
+    suite.addTest(api.suite())
+    suite.addTest(auth.suite())
+    suite.addTest(cgi_frontend.suite())
+    suite.addTest(chrome.suite())
+    suite.addTest(clearsilver.suite())
+    suite.addTest(href.suite())
+    suite.addTest(session.suite())
+    suite.addTest(wikisyntax.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/tests/api.py
@@ -0,0 +1,106 @@
+# -*- coding: utf-8 -*-
+
+from trac.test import Mock
+from trac.web.api import Request, RequestDone
+from trac.web.clearsilver import HDFWrapper
+
+from Cookie import SimpleCookie as Cookie
+from StringIO import StringIO
+import unittest
+
+
+class RequestTestCase(unittest.TestCase):
+
+    def _make_environ(self, scheme='http', server_name='example.org',
+                      server_port=80, method='GET', script_name='/trac',
+                      **kwargs):
+        environ = {'wsgi.url_scheme': scheme, 'wsgi.input': StringIO(''),
+                   'REQUEST_METHOD': method, 'SERVER_NAME': server_name,
+                   'SERVER_PORT': server_port, 'SCRIPT_NAME': script_name}
+        environ.update(kwargs)
+        return environ
+
+    def test_base_url(self):
+        environ = self._make_environ()
+        req = Request(environ, None)
+        self.assertEqual('http://example.org/trac', req.base_url)
+
+    def test_base_url_host(self):
+        environ = self._make_environ(server_port=8080, HTTP_HOST='example.com')
+        req = Request(environ, None)
+        self.assertEqual('http://example.com/trac', req.base_url)
+
+    def test_base_url_nondefaultport(self):
+        environ = self._make_environ(server_port=8080)
+        req = Request(environ, None)
+        self.assertEqual('http://example.org:8080/trac', req.base_url)
+
+    def test_base_url_https(self):
+        environ = self._make_environ(scheme='https', server_port=443)
+        req = Request(environ, None)
+        self.assertEqual('https://example.org/trac', req.base_url)
+
+    def test_base_url_https_host(self):
+        environ = self._make_environ(scheme='https', server_port=443,
+                                     HTTP_HOST='example.com')
+        req = Request(environ, None)
+        self.assertEqual('https://example.com/trac', req.base_url)
+
+    def test_base_url_https_nondefaultport(self):
+        environ = self._make_environ(scheme='https', server_port=8443)
+        req = Request(environ, None)
+        self.assertEqual('https://example.org:8443/trac', req.base_url)
+
+    def test_base_url_proxy(self):
+        environ = self._make_environ(HTTP_HOST='localhost',
+                                     HTTP_X_FORWARDED_HOST='example.com')
+        req = Request(environ, None)
+        self.assertEqual('http://localhost/trac', req.base_url)
+
+    def test_redirect(self):
+        status_sent = []
+        headers_sent = {}
+        def start_response(status, headers):
+            status_sent.append(status)
+            headers_sent.update(dict(headers))
+        environ = self._make_environ(method='HEAD')
+        req = Request(environ, start_response)
+        self.assertRaises(RequestDone, req.redirect, '/trac/test')
+        self.assertEqual('302 Found', status_sent[0])
+        self.assertEqual('http://example.org/trac/test',
+                         headers_sent['Location'])
+
+    def test_redirect_absolute(self):
+        status_sent = []
+        headers_sent = {}
+        def start_response(status, headers):
+            status_sent.append(status)
+            headers_sent.update(dict(headers))
+        environ = self._make_environ(method='HEAD')
+        req = Request(environ, start_response)
+        self.assertRaises(RequestDone, req.redirect,
+                          'http://example.com/trac/test')
+        self.assertEqual('302 Found', status_sent[0])
+        self.assertEqual('http://example.com/trac/test',
+                         headers_sent['Location'])
+
+    def test_write_unicode(self):
+        buf = StringIO()
+        def write(data):
+            buf.write(data)
+        def start_response(status, headers):
+            return write
+        environ = self._make_environ(method='HEAD')
+        req = Request(environ, start_response)
+        req.send_header('Content-Type', 'text/plain;charset=utf-8')
+        req.write(u'Föö')
+        self.assertEqual('Föö', buf.getvalue())
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(RequestTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/tests/auth.py
@@ -0,0 +1,164 @@
+from trac.test import EnvironmentStub, Mock
+from trac.web.auth import LoginModule
+from trac.web.href import Href
+
+from Cookie import SimpleCookie as Cookie
+import unittest
+
+
+class LoginModuleTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.db = self.env.get_db_cnx()
+        self.module = LoginModule(self.env)
+
+    def test_anonymous_access(self):
+        req = Mock(incookie=Cookie(), href=Href('/trac.cgi'),
+                   remote_addr='127.0.0.1', remote_user=None)
+        self.assertEqual(None, self.module.authenticate(req))
+
+    def test_unknown_cookie_access(self):
+        incookie = Cookie()
+        incookie['trac_auth'] = '123'
+        req = Mock(cgi_location='/trac', href=Href('/trac.cgi'),
+                   incookie=incookie, outcookie=Cookie(),
+                   remote_addr='127.0.0.1', remote_user=None)
+        self.assertEqual(None, self.module.authenticate(req))
+
+    def test_known_cookie_access(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO auth_cookie (cookie, name, ipnr) "
+                       "VALUES ('123', 'john', '127.0.0.1')")
+        incookie = Cookie()
+        incookie['trac_auth'] = '123'
+        outcookie = Cookie()
+        req = Mock(incookie=incookie, outcookie=outcookie,
+                   href=Href('/trac.cgi'),
+                   remote_addr='127.0.0.1', remote_user=None)
+        self.assertEqual('john', self.module.authenticate(req))
+        self.failIf('auth_cookie' in req.outcookie)
+
+    def test_known_cookie_different_ipnr_access(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO auth_cookie (cookie, name, ipnr) "
+                       "VALUES ('123', 'john', '127.0.0.1')")
+        incookie = Cookie()
+        incookie['trac_auth'] = '123'
+        outcookie = Cookie()
+        req = Mock(cgi_location='/trac', href=Href('/trac.cgi'),
+                   incookie=incookie, outcookie=outcookie,
+                   remote_addr='192.168.0.100', remote_user=None)
+        self.assertEqual(None, self.module.authenticate(req))
+        self.failIf('trac_auth' not in req.outcookie)
+
+    def test_known_cookie_ip_check_disabled(self):
+        self.env.config.set('trac', 'check_auth_ip', 'no')
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO auth_cookie (cookie, name, ipnr) "
+                       "VALUES ('123', 'john', '127.0.0.1')")
+        incookie = Cookie()
+        incookie['trac_auth'] = '123'
+        outcookie = Cookie()
+        req = Mock(incookie=incookie, outcookie=outcookie,
+                   href=Href('/trac.cgi'),
+                   remote_addr='192.168.0.100', remote_user=None)
+        self.assertEqual('john', self.module.authenticate(req))
+        self.failIf('auth_cookie' in req.outcookie)
+
+    def test_login(self):
+        outcookie = Cookie()
+        # remote_user must be upper case to test that by default, case is
+        # preserved.
+        req = Mock(cgi_location='/trac', href=Href('/trac.cgi'),
+                   incookie=Cookie(), outcookie=outcookie,
+                   remote_addr='127.0.0.1', remote_user='john', authname='john')
+        self.module._do_login(req)
+
+        assert outcookie.has_key('trac_auth'), '"trac_auth" Cookie not set'
+        auth_cookie = outcookie['trac_auth'].value
+        cursor = self.db.cursor()
+        cursor.execute("SELECT name,ipnr FROM auth_cookie WHERE cookie=%s",
+                       (auth_cookie,))
+        row = cursor.fetchone()
+        self.assertEquals('john', row[0])
+        self.assertEquals('127.0.0.1', row[1])
+    
+    def test_login_ignore_case(self):
+        """
+        Test that login is succesful when the usernames differ in case, but case
+        is ignored.
+        """
+        self.env.config.set('trac', 'ignore_auth_case', 'yes')
+
+        outcookie = Cookie()
+        req = Mock(cgi_location='/trac', href=Href('/trac.cgi'),
+                   incookie=Cookie(), outcookie=outcookie,
+                   remote_addr='127.0.0.1', remote_user='John',
+                   authname='anonymous')
+        self.module._do_login(req)
+
+        assert outcookie.has_key('trac_auth'), '"trac_auth" Cookie not set'
+        auth_cookie = outcookie['trac_auth'].value
+        cursor = self.db.cursor()
+        cursor.execute("SELECT name,ipnr FROM auth_cookie WHERE cookie=%s",
+                       (auth_cookie,))
+        row = cursor.fetchone()
+        self.assertEquals('john', row[0])
+        self.assertEquals('127.0.0.1', row[1])
+
+    def test_login_no_username(self):
+        req = Mock(incookie=Cookie(), href=Href('/trac.cgi'),
+                   remote_addr='127.0.0.1', remote_user=None)
+        self.assertRaises(AssertionError, self.module._do_login, req)
+
+    def test_already_logged_in_same_user(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO auth_cookie (cookie, name, ipnr) "
+                       "VALUES ('123', 'john', '127.0.0.1')")
+        incookie = Cookie()
+        incookie['trac_auth'] = '123'
+        req = Mock(incookie=incookie, outcookie=Cookie(),
+                   href=Href('/trac.cgi'),
+                   remote_addr='127.0.0.1', remote_user='john', authname='john')
+        self.module._do_login(req) # this shouldn't raise an error
+
+    def test_already_logged_in_different_user(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO auth_cookie (cookie, name, ipnr) "
+                       "VALUES ('123', 'john', '127.0.0.1')")
+        incookie = Cookie()
+        incookie['trac_auth'] = '123'
+        req = Mock(incookie=incookie, authname='john',
+                   href=Href('/trac.cgi'),
+                   remote_addr='127.0.0.1', remote_user='tom')
+        self.assertRaises(AssertionError, self.module._do_login, req)
+
+    def test_logout(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO auth_cookie (cookie, name, ipnr) "
+                       "VALUES ('123', 'john', '127.0.0.1')")
+        incookie = Cookie()
+        incookie['trac_auth'] = '123'
+        outcookie = Cookie()
+        req = Mock(cgi_location='/trac', href=Href('/trac.cgi'),
+                   incookie=incookie, outcookie=outcookie,
+                   remote_addr='127.0.0.1', remote_user=None, authname='john')
+        self.module._do_logout(req)
+        self.failIf('trac_auth' not in outcookie)
+        cursor.execute("SELECT name,ipnr FROM auth_cookie WHERE name='john'")
+        self.failIf(cursor.fetchone())
+
+    def test_logout_not_logged_in(self):
+        req = Mock(cgi_location='/trac', href=Href('/trac.cgi'),
+                   incookie=Cookie(), outcookie=Cookie(),
+                   remote_addr='127.0.0.1', remote_user=None,
+                   authname='anonymous')
+        self.module._do_logout(req) # this shouldn't raise an error
+
+
+def suite():
+    return unittest.makeSuite(LoginModuleTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/tests/cgi_frontend.py
@@ -0,0 +1,14 @@
+#from trac.web.cgi_frontend import CGIRequest
+
+import unittest
+
+
+class CGIRequestTestCase(unittest.TestCase):
+    pass
+
+
+def suite():
+    return unittest.makeSuite(CGIRequestTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/tests/chrome.py
@@ -0,0 +1,210 @@
+from trac.config import Configuration
+from trac.core import Component, ComponentManager, implements
+from trac.perm import PermissionCache
+from trac.test import EnvironmentStub, Mock
+from trac.web.clearsilver import HDFWrapper
+from trac.web.chrome import add_link, add_stylesheet, Chrome, \
+                            INavigationContributor
+from trac.web.href import Href
+
+import unittest
+
+
+class ChromeTestCase(unittest.TestCase):
+
+    def test_add_link_simple(self):
+        req = Mock(hdf=HDFWrapper(), href=Href('/trac.cgi'))
+        add_link(req, 'start', '/trac/wiki')
+        self.assertEqual('/trac/wiki', req.hdf['chrome.links.start.0.href'])
+
+    def test_add_link_advanced(self):
+        req = Mock(hdf=HDFWrapper(), href=Href('/trac.cgi'))
+        add_link(req, 'start', '/trac/wiki', 'Start page', 'text/html', 'home')
+        self.assertEqual('/trac/wiki', req.hdf['chrome.links.start.0.href'])
+        self.assertEqual('Start page', req.hdf['chrome.links.start.0.title'])
+        self.assertEqual('text/html', req.hdf['chrome.links.start.0.type'])
+        self.assertEqual('home', req.hdf['chrome.links.start.0.class'])
+
+    def test_add_stylesheet(self):
+        req = Mock(base_path='/trac.cgi', hdf=HDFWrapper(), href=Href('/trac.cgi'))
+        add_stylesheet(req, 'common/css/trac.css')
+        self.assertEqual('text/css', req.hdf['chrome.links.stylesheet.0.type'])
+        self.assertEqual('/trac.cgi/chrome/common/css/trac.css',
+                         req.hdf['chrome.links.stylesheet.0.href'])
+
+    def test_htdocs_location(self):
+        env = EnvironmentStub(enable=[])
+        req = Mock(hdf=HDFWrapper(), href=Href('/trac.cgi'),
+                   base_path='/trac.cgi', path_info='')
+        Chrome(env).populate_hdf(req, None)
+        self.assertEqual('/trac.cgi/chrome/common/', req.hdf['htdocs_location'])
+
+    def test_logo(self):
+        env = EnvironmentStub(enable=[])
+        req = Mock(hdf=HDFWrapper(), href=Href('/trac.cgi'),
+                   base_path='/trac.cgi', path_info='')
+
+        # Verify that no logo data is put in the HDF if no logo is configured
+        env.config.set('header_logo', 'src', '')
+        Chrome(env).populate_hdf(req, None)
+        assert 'chrome.logo.src' not in req.hdf
+
+        # Test with a relative path to the logo image
+        req.hdf = HDFWrapper()
+        env.config.set('header_logo', 'src', 'foo.png')
+        Chrome(env).populate_hdf(req, None)
+        self.assertEqual('/trac.cgi/chrome/common/foo.png',
+                         req.hdf['chrome.logo.src'])
+
+        # Test with a server-relative path to the logo image
+        req.hdf = HDFWrapper()
+        env.config.set('header_logo', 'src', '/img/foo.png')
+        Chrome(env).populate_hdf(req, None)
+        self.assertEqual('/img/foo.png', req.hdf['chrome.logo.src'])
+
+        # Test with an absolute path to the logo image
+        req.hdf = HDFWrapper()
+        env.config.set('header_logo', 'src', 'http://www.example.org/foo.png')
+        Chrome(env).populate_hdf(req, None)
+        self.assertEqual('http://www.example.org/foo.png',
+                         req.hdf['chrome.logo.src'])
+
+    def test_default_links(self):
+        env = EnvironmentStub(enable=[])
+        req = Mock(hdf=HDFWrapper(), href=Href('/trac.cgi'),
+                   base_path='/trac.cgi', path_info='')
+        Chrome(env).populate_hdf(req, None)
+        self.assertEqual('/trac.cgi/wiki',
+                         req.hdf['chrome.links.start.0.href'])
+        self.assertEqual('/trac.cgi/search',
+                         req.hdf['chrome.links.search.0.href'])
+        self.assertEqual('/trac.cgi/wiki/TracGuide',
+                         req.hdf['chrome.links.help.0.href'])
+        self.assertEqual('/trac.cgi/chrome/common/css/trac.css',
+                         req.hdf['chrome.links.stylesheet.0.href'])
+
+    def test_icon_links(self):
+        env = EnvironmentStub(enable=[])
+        req = Mock(hdf=HDFWrapper(), href=Href('/trac.cgi'),
+                   base_path='/trac.cgi', path_info='')
+
+        # No icon set in config, so no icon links
+        env.config.set('project', 'icon', '')
+        Chrome(env).populate_hdf(req, None)
+        assert 'chrome.links.icon' not in req.hdf
+        assert 'chrome.links.shortcut icon' not in req.hdf
+
+        # Relative URL for icon config option
+        env.config.set('project', 'icon', 'trac.ico')
+        Chrome(env).populate_hdf(req, None)
+        self.assertEqual('/trac.cgi/chrome/common/trac.ico',
+                         req.hdf['chrome.links.icon.0.href'])
+        self.assertEqual('/trac.cgi/chrome/common/trac.ico',
+                         req.hdf['chrome.links.shortcut icon.0.href'])
+
+        # URL relative to the server root for icon config option
+        req.hdf = HDFWrapper()
+        env.config.set('project', 'icon', '/favicon.ico')
+        Chrome(env).populate_hdf(req, None)
+        self.assertEqual('/favicon.ico',
+                         req.hdf['chrome.links.icon.0.href'])
+        self.assertEqual('/favicon.ico',
+                         req.hdf['chrome.links.shortcut icon.0.href'])
+
+        # Absolute URL for icon config option
+        req.hdf = HDFWrapper()
+        env.config.set('project', 'icon', 'http://example.com/favicon.ico')
+        Chrome(env).populate_hdf(req, None)
+        self.assertEqual('http://example.com/favicon.ico',
+                         req.hdf['chrome.links.icon.0.href'])
+        self.assertEqual('http://example.com/favicon.ico',
+                         req.hdf['chrome.links.shortcut icon.0.href'])
+
+    def test_nav_contributor(self):
+        class TestNavigationContributor(Component):
+            implements(INavigationContributor)
+            def get_active_navigation_item(self, req):
+                return None
+            def get_navigation_items(self, req):
+                yield 'metanav', 'test', 'Test'
+        env = EnvironmentStub(enable=[TestNavigationContributor])
+        req = Mock(hdf=HDFWrapper(), href=Href('/trac.cgi'),
+                   path_info='/', base_path='/trac.cgi')
+        chrome = Chrome(env)
+        chrome.populate_hdf(req, None)
+        self.assertEqual('Test', req.hdf['chrome.nav.metanav.test'])
+        self.assertRaises(KeyError, req.hdf.__getitem__,
+                          'chrome.nav.metanav.test.active')
+
+    def test_nav_contributor_active(self):
+        class TestNavigationContributor(Component):
+            implements(INavigationContributor)
+            def get_active_navigation_item(self, req):
+                return 'test'
+            def get_navigation_items(self, req):
+                yield 'metanav', 'test', 'Test'
+        env = EnvironmentStub(enable=[TestNavigationContributor])
+        req = Mock(hdf=HDFWrapper(), href=Href('/trac.cgi'),
+                   path_info='/', base_path='/trac.cgi')
+        chrome = Chrome(env)
+        chrome.populate_hdf(req, TestNavigationContributor(env))
+        self.assertEqual('Test', req.hdf['chrome.nav.metanav.test'])
+        self.assertEqual('1', req.hdf['chrome.nav.metanav.test.active'])
+
+    def test_nav_contributor_order(self):
+        class TestNavigationContributor1(Component):
+            implements(INavigationContributor)
+            def get_active_navigation_item(self, req):
+                return None
+            def get_navigation_items(self, req):
+                yield 'metanav', 'test1', 'Test 1'
+        class TestNavigationContributor2(Component):
+            implements(INavigationContributor)
+            def get_active_navigation_item(self, req):
+                return None
+            def get_navigation_items(self, req):
+                yield 'metanav', 'test2', 'Test 2'
+        env = EnvironmentStub(enable=[TestNavigationContributor1,
+                                      TestNavigationContributor2])
+        req = Mock(hdf=HDFWrapper(), href=Href('/trac.cgi'),
+                   path_info='/', base_path='/trac.cgi')
+        chrome = Chrome(env)
+
+        # Test with both items set in the order option
+        env.config.set('trac', 'metanav', 'test2, test1')
+        chrome.populate_hdf(req, None)
+        node = req.hdf.getObj('chrome.nav.metanav').child()
+        self.assertEqual('test2', node.name())
+        self.assertEqual('test1', node.next().name())
+
+        # Test with only test1 in the order options
+        req.hdf = HDFWrapper()
+        env.config.set('trac', 'metanav', 'test1')
+        chrome.populate_hdf(req, None)
+        node = req.hdf.getObj('chrome.nav.metanav').child()
+        self.assertEqual('test1', node.name())
+        self.assertEqual('test2', node.next().name())
+
+        # Test with only test2 in the order options
+        req.hdf = HDFWrapper()
+        env.config.set('trac', 'metanav', 'test2')
+        chrome.populate_hdf(req, None)
+        node = req.hdf.getObj('chrome.nav.metanav').child()
+        self.assertEqual('test2', node.name())
+        self.assertEqual('test1', node.next().name())
+
+        # Test with none in the order options (order corresponds to
+        # registration order)
+        req.hdf = HDFWrapper()
+        env.config.set('trac', 'metanav', 'foo, bar')
+        chrome.populate_hdf(req, None)
+        node = req.hdf.getObj('chrome.nav.metanav').child()
+        self.assertEqual('test1', node.name())
+        self.assertEqual('test2', node.next().name())
+
+
+def suite():
+    return unittest.makeSuite(ChromeTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/tests/clearsilver.py
@@ -0,0 +1,17 @@
+from trac.web import clearsilver
+
+import unittest
+
+
+def suite():
+    try:
+        from doctest import DocTestSuite
+        return DocTestSuite(clearsilver)
+    except ImportError:
+        import sys
+        print>>sys.stderr, "WARNING: DocTestSuite required to run these tests"
+    return unittest.TestSuite()
+
+if __name__ == '__main__':
+    runner = unittest.TextTestRunner()
+    runner.run(suite())
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/tests/href.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+from trac.web import href
+
+import sys
+import unittest
+
+
+def suite():
+    try:
+        from doctest import DocTestSuite
+        return DocTestSuite(href)
+    except ImportError:
+        print>>sys.stderr, "DocTestSuite not available, skipping href tests"
+        return unittest.TestSuite()
+
+if __name__ == '__main__':
+    runner = unittest.TextTestRunner()
+    runner.run(suite())
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/tests/session.py
@@ -0,0 +1,294 @@
+from Cookie import SimpleCookie as Cookie
+import time
+import unittest
+
+from trac.core import TracError
+from trac.log import logger_factory
+from trac.test import EnvironmentStub, Mock
+from trac.web.href import Href
+from trac.web.session import Session, PURGE_AGE, UPDATE_INTERVAL
+
+
+class SessionTestCase(unittest.TestCase):
+    """Unit tests for the persistent session support."""
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.db = self.env.get_db_cnx()
+
+    def test_new_session(self):
+        """
+        Verify that a session cookie gets sent back to the client for a new
+        session.
+        """
+        cookie = Cookie()
+        req = Mock(incookie=Cookie(), outcookie=cookie, authname='anonymous',
+                   base_path='/')
+        session = Session(self.env, req)
+        self.assertEqual(session.sid, cookie['trac_session'].value)
+        cursor = self.db.cursor()
+        cursor.execute("SELECT COUNT(*) FROM session")
+        self.assertEqual(0, cursor.fetchone()[0])
+
+    def test_anonymous_session(self):
+        """
+        Verify that session variables are stored in the database.
+        """
+        incookie = Cookie()
+        incookie['trac_session'] = '123456'
+        outcookie = Cookie()
+        req = Mock(authname='anonymous', base_path='/', incookie=incookie,
+                   outcookie=outcookie)
+        session = Session(self.env, req)
+        self.assertEquals('123456', session.sid)
+        self.failIf(outcookie.has_key('trac_session'))
+
+    def test_authenticated_session(self):
+        """
+        Verifies that a session cookie does not get used if the user is logged
+        in, and that Trac expires the cookie.
+        """
+        incookie = Cookie()
+        incookie['trac_session'] = '123456'
+        outcookie = Cookie()
+        req = Mock(authname='john', base_path='/', incookie=incookie,
+                   outcookie=outcookie)
+        session = Session(self.env, req)
+        self.assertEqual('john', session.sid)
+        session['foo'] = 'bar'
+        session.save()
+        self.assertEquals(0, outcookie['trac_session']['expires'])
+
+    def test_session_promotion(self):
+        """
+        Verifies that an existing anonymous session gets promoted to an
+        authenticated session when the user logs in.
+        """
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO session VALUES ('123456', 0, 0)")
+        incookie = Cookie()
+        incookie['trac_session'] = '123456'
+        outcookie = Cookie()
+        req = Mock(authname='john', base_path='/', incookie=incookie,
+                   outcookie=outcookie)
+        session = Session(self.env, req)
+        self.assertEqual('john', session.sid)
+        session.save()
+
+        cursor.execute("SELECT sid,authenticated FROM session")
+        self.assertEqual(('john', 1), cursor.fetchone())
+        self.assertEqual(None, cursor.fetchone())
+
+    def test_new_session_promotion(self):
+        """
+        Verifies that even without a preexisting anonymous session,
+        an authenticated session will be created when the user logs in.
+        (same test as above without the initial INSERT)
+        """
+        cursor = self.db.cursor()
+        incookie = Cookie()
+        incookie['trac_session'] = '123456'
+        outcookie = Cookie()
+        req = Mock(authname='john', base_path='/', incookie=incookie,
+                   outcookie=outcookie)
+        session = Session(self.env, req)
+        self.assertEqual('john', session.sid)
+        session.save()
+
+        cursor.execute("SELECT sid,authenticated FROM session")
+        self.assertEqual(('john', 1), cursor.fetchone())
+        self.assertEqual(None, cursor.fetchone())
+
+    def test_add_anonymous_session_var(self):
+        """
+        Verify that new variables are inserted into the 'session' table in the
+        database for an anonymous session.
+        """
+        incookie = Cookie()
+        incookie['trac_session'] = '123456'
+        req = Mock(authname='anonymous', base_path='/', incookie=incookie,
+                   outcookie=Cookie())
+        session = Session(self.env, req)
+        session['foo'] = 'bar'
+        session.save()
+        cursor = self.db.cursor()
+        cursor.execute("SELECT value FROM session_attribute WHERE sid='123456'")
+        self.assertEqual('bar', cursor.fetchone()[0])
+
+    def test_modify_anonymous_session_var(self):
+        """
+        Verify that modifying an existing variable updates the 'session' table
+        accordingly for an anonymous session.
+        """
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO session VALUES ('123456', 0, 0)")
+        cursor.execute("INSERT INTO session_attribute VALUES "
+                       "('123456', 0, 'foo', 'bar')")
+        incookie = Cookie()
+        incookie['trac_session'] = '123456'
+        req = Mock(authname='anonymous', base_path='/', incookie=incookie,
+                   outcookie=Cookie())
+        session = Session(self.env, req)
+        self.assertEqual('bar', session['foo'])
+        session['foo'] = 'baz'
+        session.save()
+        cursor.execute("SELECT value FROM session_attribute WHERE sid='123456'")
+        self.assertEqual('baz', cursor.fetchone()[0])
+
+    def test_delete_anonymous_session_var(self):
+        """
+        Verify that modifying a variable updates the 'session' table accordingly
+        for an anonymous session.
+        """
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO session VALUES ('123456', 0, 0)")
+        cursor.execute("INSERT INTO session_attribute VALUES "
+                       "('123456', 0, 'foo', 'bar')")
+        incookie = Cookie()
+        incookie['trac_session'] = '123456'
+        req = Mock(authname='anonymous', base_path='/', incookie=incookie,
+                   outcookie=Cookie())
+        session = Session(self.env, req)
+        self.assertEqual('bar', session['foo'])
+        del session['foo']
+        session.save()
+        cursor.execute("SELECT COUNT(*) FROM session_attribute "
+                       "WHERE sid='123456' AND name='foo'") 
+        self.assertEqual(0, cursor.fetchone()[0])
+
+    def test_purge_anonymous_session(self):
+        """
+        Verify that old sessions get purged.
+        """
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO session "
+                       "VALUES ('987654', 0, %s)",
+                       (time.time() - PURGE_AGE - 3600,))
+        cursor.execute("INSERT INTO session_attribute VALUES "
+                       "('987654', 0, 'foo', 'bar')")
+        
+        # We need to modify a different session to trigger the purging
+        incookie = Cookie()
+        incookie['trac_session'] = '123456'
+        req = Mock(authname='anonymous', base_path='/', incookie=incookie,
+                   outcookie=Cookie())
+        session = Session(self.env, req)
+        session['foo'] = 'bar'
+        session.save()
+
+        cursor.execute("SELECT COUNT(*) FROM session WHERE sid='987654' AND "
+                       "authenticated=0")
+        self.assertEqual(0, cursor.fetchone()[0])
+
+    def test_delete_empty_session(self):
+        """
+        Verify that a session gets deleted when it doesn't have any data except
+        for the 'last_visit' timestamp.
+        """
+        now = time.time()
+
+        # Make sure the session has data so that it doesn't get dropped
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO session "
+                       "VALUES ('123456', 0, %s)",
+                       (int(now - UPDATE_INTERVAL - 3600),))
+        cursor.execute("INSERT INTO session_attribute VALUES "
+                       "('123456', 0, 'foo', 'bar')")
+
+        incookie = Cookie()
+        incookie['trac_session'] = '123456'
+        req = Mock(authname='anonymous', base_path='/', incookie=incookie,
+                   outcookie=Cookie())
+        session = Session(self.env, req)
+        del session['foo']
+        session.save()
+
+        cursor.execute("SELECT COUNT(*) FROM session WHERE sid='123456' AND "
+                       "authenticated=0")
+        self.assertEqual(0, cursor.fetchone()[0])
+
+    def test_add_authenticated_session_var(self):
+        """
+        Verify that new variables are inserted into the 'session' table in the
+        database for an authenticated session.
+        """
+        req = Mock(authname='john', base_path='/', incookie=Cookie())
+        session = Session(self.env, req)
+        session['foo'] = 'bar'
+        session.save()
+        cursor = self.db.cursor()
+        cursor.execute("SELECT value FROM session_attribute WHERE sid='john'"
+                       "AND name='foo'") 
+        self.assertEqual('bar', cursor.fetchone()[0])
+
+    def test_modify_authenticated_session_var(self):
+        """
+        Verify that modifying an existing variable updates the 'session' table
+        accordingly for an authenticated session.
+        """
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO session VALUES ('john', 1, 0)")
+        cursor.execute("INSERT INTO session_attribute VALUES "
+                       "('john', 1, 'foo', 'bar')")
+
+        req = Mock(authname='john', base_path='/', incookie=Cookie())
+        session = Session(self.env, req)
+        self.assertEqual('bar', session['foo'])
+        session['foo'] = 'baz'
+        session.save()
+        cursor.execute("SELECT value FROM session_attribute "
+                       "WHERE sid='john' AND name='foo'") 
+        self.assertEqual('baz', cursor.fetchone()[0])
+
+    def test_delete_authenticated_session_var(self):
+        """
+        Verify that modifying a variable updates the 'session' table accordingly
+        for an authenticated session.
+        """
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO session VALUES ('john', 1, 0)")
+        cursor.execute("INSERT INTO session_attribute VALUES "
+                       "('john', 1, 'foo', 'bar')")
+
+        req = Mock(authname='john', base_path='/', incookie=Cookie())
+        session = Session(self.env, req)
+        self.assertEqual('bar', session['foo'])
+        del session['foo']
+        session.save()
+        cursor.execute("SELECT COUNT(*) FROM session_attribute "
+                       "WHERE sid='john' AND name='foo'") 
+        self.assertEqual(0, cursor.fetchone()[0])
+
+    def test_update_session(self):
+        """
+        Verify that accessing a session after one day updates the sessions 
+        'last_visit' variable so that the session doesn't get purged.
+        """
+        now = time.time()
+
+        # Make sure the session has data so that it doesn't get dropped
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO session VALUES ('123456', 0, 1)")
+        cursor.execute("INSERT INTO session_attribute VALUES "
+                       "('123456', 0, 'foo', 'bar')")
+
+        incookie = Cookie()
+        incookie['trac_session'] = '123456'
+        outcookie = Cookie()
+        req = Mock(authname='anonymous', base_path='/', incookie=incookie,
+                   outcookie=outcookie)
+        session = Session(self.env, req)
+        session.save() # updating should not require modifications
+
+        self.assertEqual(PURGE_AGE, outcookie['trac_session']['expires'])
+
+        cursor.execute("SELECT last_visit FROM session WHERE sid='123456' AND "
+                       "authenticated=0")
+        self.assertAlmostEqual(now, int(cursor.fetchone()[0]), -1)
+
+
+def suite():
+    return unittest.makeSuite(SessionTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main()
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/tests/wikisyntax.py
@@ -0,0 +1,25 @@
+import unittest
+
+from trac.web.chrome import Chrome
+from trac.wiki.tests import formatter
+
+TEST_CASES="""
+============================== htdocs: links resolver
+htdocs:release-1.0.tar.gz
+
+[htdocs:release-1.0.tar.gz Release 1.0]
+------------------------------
+<p>
+<a href="/chrome/site/release-1.0.tar.gz">htdocs:release-1.0.tar.gz</a>
+</p>
+<p>
+<a href="/chrome/site/release-1.0.tar.gz">Release 1.0</a>
+</p>
+------------------------------
+"""
+
+def suite():
+    return formatter.suite(TEST_CASES,file=__file__)
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/web/wsgi.py
@@ -0,0 +1,216 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005 Edgewall Software
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+import sys
+from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
+from SocketServer import ForkingMixIn, ThreadingMixIn
+import urllib
+
+
+class _ErrorsWrapper(object):
+
+    def __init__(self, logfunc):
+        self.logfunc = logfunc
+
+    def flush(self):
+        pass
+
+    def write(self, msg):
+        self.logfunc(msg)
+
+    def writelines(self, seq):
+        map(self.write, seq)
+
+
+class _FileWrapper(object):
+    """Wrapper for sending a file as response."""
+
+    def __init__(self, fileobj, blocksize=None):
+        self.fileobj = fileobj
+        self.blocksize = blocksize
+        self.read = self.fileobj.read
+        if hasattr(fileobj, 'close'):
+            self.close = fileobj.close
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        data = self.fileobj.read(self.blocksize)
+        if not data:
+            raise StopIteration
+        return data
+
+
+class WSGIGateway(object):
+    """Abstract base class for WSGI servers or gateways."""
+
+    wsgi_version = (1, 0)
+    wsgi_multithread = True
+    wsgi_multiprocess = True
+    wsgi_run_once = False
+    wsgi_file_wrapper = _FileWrapper
+
+    def __init__(self, environ, stdin=sys.stdin, stderr=sys.stderr):
+        """Initialize the gateway object."""
+        environ['wsgi.version'] = self.wsgi_version
+        environ['wsgi.url_scheme'] = 'http'
+        if environ.get('HTTPS', '').lower() in ('yes', 'on', '1'):
+            environ['wsgi.url_scheme'] = 'https'
+        environ['wsgi.input'] = stdin
+        environ['wsgi.errors'] = stderr
+        environ['wsgi.multithread'] = self.wsgi_multithread
+        environ['wsgi.multiprocess'] = self.wsgi_multiprocess
+        environ['wsgi.run_once'] = self.wsgi_run_once
+        if self.wsgi_file_wrapper is not None:
+            environ['wsgi.file_wrapper'] = self.wsgi_file_wrapper
+        self.environ = environ
+
+        self.headers_set = []
+        self.headers_sent = []
+
+    def run(self, application):
+        """Start the gateway with the given WSGI application."""
+        response = application(self.environ, self._start_response)
+        try:
+            if isinstance(response, self.wsgi_file_wrapper) \
+                    and hasattr(self, '_sendfile'):
+                self._sendfile(response.fileobj)
+            else:
+                for chunk in response:
+                    if chunk:
+                        self._write(chunk)
+                if not self.headers_sent:
+                    self._write('')
+        finally:
+            if hasattr(response, 'close'):
+                response.close()
+
+    def _start_response(self, status, headers, exc_info=None):
+        """Callback for starting a HTTP response."""
+        if exc_info:
+            try:
+                if self.headers_sent: # Re-raise original exception
+                    raise exc_info[0], exc_info[1], exc_info[2]
+            finally:
+                exc_info = None # avoid dangling circular ref
+        else:
+            assert not self.headers_set, 'Response already started'
+
+        self.headers_set = [status, headers]
+        return self._write
+
+    def _write(self, data):
+        """Callback for writing data to the response.
+        
+        Concrete subclasses must implement this method."""
+        raise NotImplementedError
+
+
+class WSGIRequestHandler(BaseHTTPRequestHandler):
+
+    def setup_environ(self):
+        self.raw_requestline = self.rfile.readline()
+        if not self.parse_request(): # An error code has been sent, just exit
+            self.close_connection = 1
+            return
+
+        environ = self.server.environ.copy()
+        environ['SERVER_PROTOCOL'] = self.request_version
+        environ['REQUEST_METHOD'] = self.command
+
+        if '?' in self.path:
+            path_info, query_string = self.path.split('?', 1)
+        else:
+            path_info, query_string = self.path, ''
+        environ['PATH_INFO'] = urllib.unquote(path_info)
+        environ['QUERY_STRING'] = query_string
+
+        host = self.address_string()
+        if host != self.client_address[0]:
+            environ['REMOTE_HOST'] = host
+        environ['REMOTE_ADDR'] = self.client_address[0]
+
+        if self.headers.typeheader is None:
+            environ['CONTENT_TYPE'] = self.headers.type
+        else:
+            environ['CONTENT_TYPE'] = self.headers.typeheader
+
+        length = self.headers.getheader('content-length')
+        if length:
+            environ['CONTENT_LENGTH'] = length
+
+        for name, value in [header.split(':', 1) for header
+                            in self.headers.headers]:
+            name = name.replace('-', '_').upper();
+            value = value.strip()
+            if name in environ:
+                # skip content length, type, etc.
+                continue
+            if 'HTTP_' + name in environ:
+                # comma-separate multiple headers
+                environ['HTTP_' + name] += ',' + value
+            else:
+                environ['HTTP_' + name] = value
+
+        return environ
+
+    def handle_one_request(self):
+        environ = self.setup_environ()
+        gateway = self.server.gateway(self, environ)
+        gateway.run(self.server.application)
+
+    def finish(self):
+        """We need to help the garbage collector a little."""
+        BaseHTTPRequestHandler.finish(self)
+        self.wfile = None
+        self.rfile = None
+
+
+class WSGIServerGateway(WSGIGateway):
+
+    def __init__(self, handler, environ):
+        WSGIGateway.__init__(self, environ, handler.rfile,
+                             _ErrorsWrapper(lambda x: handler.log_error('%s', x)))
+        self.handler = handler
+
+    def _write(self, data):
+        assert self.headers_set, 'Response not started'
+
+        if not self.headers_sent:
+            status, headers = self.headers_sent = self.headers_set
+            self.handler.send_response(int(status[:3]))
+            for name, value in headers:
+                self.handler.send_header(name, value)
+            self.handler.end_headers()
+        self.handler.wfile.write(data)
+
+
+class WSGIServer(HTTPServer):
+
+    def __init__(self, server_address, application, gateway=WSGIServerGateway,
+                 request_handler=WSGIRequestHandler):
+        HTTPServer.__init__(self, server_address, request_handler)
+
+        self.application = application
+
+        gateway.wsgi_multithread = isinstance(self, ThreadingMixIn)
+        gateway.wsgi_multiprocess = isinstance(self, ForkingMixIn)
+        self.gateway = gateway
+
+        self.environ = {'SERVER_NAME': self.server_name,
+                        'SERVER_PORT': str(self.server_port),
+                        'SCRIPT_NAME': ''}
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/wiki/__init__.py
@@ -0,0 +1,3 @@
+from trac.wiki.api import *
+from trac.wiki.formatter import *
+from trac.wiki.model import *
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/wiki/api.py
@@ -0,0 +1,264 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+try:
+    import threading
+except ImportError:
+    import dummy_threading as threading
+import time
+import urllib
+import re
+
+from trac.config import BoolOption
+from trac.core import *
+from trac.util.markup import html
+
+
+class IWikiChangeListener(Interface):
+    """Extension point interface for components that should get notified about
+    the creation, deletion and modification of wiki pages.
+    """
+
+    def wiki_page_added(page):
+        """Called whenever a new Wiki page is added."""
+
+    def wiki_page_changed(page, version, t, comment, author, ipnr):
+        """Called when a page has been modified."""
+
+    def wiki_page_deleted(page):
+        """Called when a page has been deleted."""
+
+    def wiki_page_version_deleted(page):
+        """Called when a version of a page has been deleted."""
+
+
+class IWikiPageManipulator(Interface):
+    """Extension point interface for components that need to to specific
+    pre and post processing of wiki page changes.
+    
+    Unlike change listeners, a manipulator can reject changes being committed
+    to the database.
+    """
+
+    def prepare_wiki_page(req, page, fields):
+        """Not currently called, but should be provided for future
+        compatibility."""
+
+    def validate_wiki_page(req, page):
+        """Validate a wiki page after it's been populated from user input.
+        
+        Must return a list of `(field, message)` tuples, one for each problem
+        detected. `field` can be `None` to indicate an overall problem with the
+        page. Therefore, a return value of `[]` means everything is OK."""
+
+
+class IWikiMacroProvider(Interface):
+    """Extension point interface for components that provide Wiki macros."""
+
+    def get_macros():
+        """Return an iterable that provides the names of the provided macros."""
+
+    def get_macro_description(name):
+        """Return a plain text description of the macro with the specified name.
+        """
+
+    def render_macro(req, name, content):
+        """Return the HTML output of the macro."""
+
+
+class IWikiSyntaxProvider(Interface):
+ 
+    def get_wiki_syntax():
+        """Return an iterable that provides additional wiki syntax.
+
+        Additional wiki syntax correspond to a pair of (regexp, cb),
+        the `regexp` for the additional syntax and the callback `cb`
+        which will be called if there's a match.
+        That function is of the form cb(formatter, ns, match).
+        """
+ 
+    def get_link_resolvers():
+        """Return an iterable over (namespace, formatter) tuples.
+
+        Each formatter should be a function of the form
+        fmt(formatter, ns, target, label), and should
+        return some HTML fragment.
+        The `label` is already HTML escaped, whereas the `target` is not.
+        """
+ 
+
+class WikiSystem(Component):
+    """Represents the wiki system."""
+
+    implements(IWikiChangeListener, IWikiSyntaxProvider)
+
+    change_listeners = ExtensionPoint(IWikiChangeListener)
+    macro_providers = ExtensionPoint(IWikiMacroProvider)
+    syntax_providers = ExtensionPoint(IWikiSyntaxProvider)
+
+    INDEX_UPDATE_INTERVAL = 5 # seconds
+
+    ignore_missing_pages = BoolOption('wiki', 'ignore_missing_pages', 'false',
+        """Enable/disable highlighting CamelCase links to missing pages
+        (''since 0.9'').""")
+
+    split_page_names = BoolOption('wiki', 'split_page_names', 'false',
+        """Enable/disable splitting the WikiPageNames with space characters
+        (''since 0.10'').""")
+
+    def __init__(self):
+        self._index = None
+        self._last_index_update = 0
+        self._index_lock = threading.RLock()
+        self._compiled_rules = None
+        self._link_resolvers = None
+        self._helper_patterns = None
+        self._external_handlers = None
+
+    def _update_index(self):
+        self._index_lock.acquire()
+        try:
+            now = time.time()
+            if now > self._last_index_update + WikiSystem.INDEX_UPDATE_INTERVAL:
+                self.log.debug('Updating wiki page index')
+                db = self.env.get_db_cnx()
+                cursor = db.cursor()
+                cursor.execute("SELECT DISTINCT name FROM wiki")
+                self._index = {}
+                for (name,) in cursor:
+                    self._index[name] = True
+                self._last_index_update = now
+        finally:
+            self._index_lock.release()
+
+    # Public API
+
+    def get_pages(self, prefix=None):
+        """Iterate over the names of existing Wiki pages.
+
+        If the `prefix` parameter is given, only names that start with that
+        prefix are included.
+        """
+        self._update_index()
+        for page in self._index.keys():
+            if not prefix or page.startswith(prefix):
+                yield page
+
+    def has_page(self, pagename):
+        """Whether a page with the specified name exists."""
+        self._update_index()
+        return self._index.has_key(pagename.rstrip('/'))
+
+    def _get_rules(self):
+        self._prepare_rules()
+        return self._compiled_rules
+    rules = property(_get_rules)
+
+    def _get_helper_patterns(self):
+        self._prepare_rules()
+        return self._helper_patterns
+    helper_patterns = property(_get_helper_patterns)
+
+    def _get_external_handlers(self):
+        self._prepare_rules()
+        return self._external_handlers
+    external_handlers = property(_get_external_handlers)
+
+    def _prepare_rules(self):
+        from trac.wiki.formatter import Formatter
+        if not self._compiled_rules:
+            helpers = []
+            handlers = {}
+            syntax = Formatter._pre_rules[:]
+            i = 0
+            for resolver in self.syntax_providers:
+                for regexp, handler in resolver.get_wiki_syntax():
+                    handlers['i' + str(i)] = handler
+                    syntax.append('(?P<i%d>%s)' % (i, regexp))
+                    i += 1
+            syntax += Formatter._post_rules[:]
+            helper_re = re.compile(r'\?P<([a-z\d_]+)>')
+            for rule in syntax:
+                helpers += helper_re.findall(rule)[1:]
+            rules = re.compile('(?:' + '|'.join(syntax) + ')')
+            self._external_handlers = handlers
+            self._helper_patterns = helpers
+            self._compiled_rules = rules
+
+    def _get_link_resolvers(self):
+        if not self._link_resolvers:
+            resolvers = {}
+            for resolver in self.syntax_providers:
+                for namespace, handler in resolver.get_link_resolvers():
+                    resolvers[namespace] = handler
+            self._link_resolvers = resolvers
+        return self._link_resolvers
+    link_resolvers = property(_get_link_resolvers)
+
+    # IWikiChangeListener methods
+
+    def wiki_page_added(self, page):
+        if not self.has_page(page.name):
+            self.log.debug('Adding page %s to index' % page.name)
+            self._index[page.name] = True
+
+    def wiki_page_changed(self, page, version, t, comment, author, ipnr):
+        pass
+
+    def wiki_page_deleted(self, page):
+        if self.has_page(page.name):
+            self.log.debug('Removing page %s from index' % page.name)
+            del self._index[page.name]
+
+    def wiki_page_version_deleted(self, page):
+        pass
+
+    # IWikiSyntaxProvider methods
+
+    def format_page_name(self, page):
+        if self.split_page_names:
+            return re.sub(r"([a-z])([A-Z][a-z])", r"\1 \2", page)
+        return page
+    
+    def get_wiki_syntax(self):
+        def wikipagenames_link(formatter, match, fullmatch):
+            return self._format_link(formatter, 'wiki', match,
+                                     self.format_page_name(match),
+                                     self.ignore_missing_pages)
+        
+        yield (r"!?(?<!/)\b" # start at a word boundary but not after '/'
+               r"[A-Z][a-z]+(?:[A-Z][a-z]*[a-z/])+" # wiki words
+               r"(?:#[A-Za-z0-9]+)?" # optional fragment identifier
+               r"(?=:?\Z|:?\s|[.,;!?\)}\]])", # what should follow it
+               wikipagenames_link)
+
+    def get_link_resolvers(self):
+        def link_resolver(formatter, ns, target, label):
+            return self._format_link(formatter, ns, target, label, False)
+        yield ('wiki', link_resolver)
+
+    def _format_link(self, formatter, ns, page, label, ignore_missing):
+        page, query, fragment = formatter.split_link(page)
+        href = formatter.href.wiki(page) + fragment
+        if not self.has_page(page):
+            if ignore_missing:
+                return label
+            return html.A(label+'?', href=href, class_='missing wiki',
+                          rel='nofollow')
+        else:
+            return html.A(label, href=href, class_='wiki')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/wiki/formatter.py
@@ -0,0 +1,1010 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+#         Christian Boos <cboos@neuf.fr>
+
+import re
+import os
+import urllib
+
+from StringIO import StringIO
+
+from trac.core import *
+from trac.mimeview import *
+from trac.wiki.api import WikiSystem
+from trac.util.text import shorten_line, to_unicode
+from trac.util.markup import escape, Markup, Element, html
+
+__all__ = ['wiki_to_html', 'wiki_to_oneliner', 'wiki_to_outline',
+           'wiki_to_link', 'Formatter' ]
+
+
+def system_message(msg, text=None):
+    return html.DIV(html.STRONG(msg), text and html.PRE(text),
+                    class_="system-message")
+
+
+class WikiProcessor(object):
+
+    _code_block_re = re.compile('^<div(?:\s+class="([^"]+)")?>(.*)</div>$')
+
+    def __init__(self, env, name):
+        # TODO: transmit `formatter` argument
+        self.env = env
+        self.name = name
+        self.error = None
+        self.macro_provider = None
+
+        builtin_processors = {'html': self._html_processor,
+                              'default': self._default_processor,
+                              'comment': self._comment_processor}
+        
+        self.processor = builtin_processors.get(name)
+        if not self.processor:
+            # Find a matching wiki macro
+            for macro_provider in WikiSystem(self.env).macro_providers:
+                for macro_name in macro_provider.get_macros():
+                    if self.name == macro_name:
+                        self.processor = self._macro_processor
+                        self.macro_provider = macro_provider
+                        break
+        if not self.processor:
+            # Find a matching mimeview renderer
+            from trac.mimeview.api import Mimeview
+            mimetype = Mimeview(self.env).get_mimetype(self.name)
+            if mimetype:
+                self.name = mimetype
+                self.processor = self._mimeview_processor
+            else:
+                self.processor = self._default_processor
+                self.error = "No macro or processor named '%s' found" % name
+
+    # builtin processors
+
+    def _comment_processor(self, req, text):
+        return ''
+
+    def _default_processor(self, req, text):
+        return html.PRE(text, class_="wiki")
+
+    def _html_processor(self, req, text):
+        from HTMLParser import HTMLParseError
+        try:
+            return Markup(text).sanitize()
+        except HTMLParseError, e:
+            self.env.log.warn(e)
+            return system_message('HTML parsing error: %s' % escape(e.msg),
+                                  text.splitlines()[e.lineno - 1].strip())
+
+    # generic processors
+
+    def _macro_processor(self, req, text):
+        # TODO: macro should take a `formatter` argument
+        self.env.log.debug('Executing Wiki macro %s by provider %s'
+                           % (self.name, self.macro_provider))
+        return self.macro_provider.render_macro(req, self.name, text)
+
+    def _mimeview_processor(self, req, text):
+        # TODO: transmit context from `formatter`
+        return Mimeview(self.env).render(req, self.name, text)
+
+    def process(self, req, text, in_paragraph=False):
+        if self.error:
+            text = system_message(Markup('Error: Failed to load processor '
+                                         '<code>%s</code>', self.name),
+                                  self.error)
+        else:
+            text = self.processor(req, text)
+        if in_paragraph:
+            content_for_span = None
+            interrupt_paragraph = False
+            if isinstance(text, Element):
+                tagname = text.tagname.lower()
+                if tagname == 'div':
+                    class_ = text.attr.get('class_', '')
+                    if class_ and 'code' in class_:
+                        content_for_span = text.children
+                    else:
+                        interrupt_paragraph = True
+                elif tagname == 'table':
+                    interrupt_paragraph = True
+            else:
+                match = re.match(self._code_block_re, text)
+                if match:
+                    if match.group(1) and 'code' in match.group(1):
+                        content_for_span = match.group(2)
+                    else:
+                        interrupt_paragraph = True
+                elif text.startswith('<table'):
+                    interrupt_paragraph = True
+            if content_for_span:
+                text = html.SPAN(content_for_span, class_='code-block')
+            elif interrupt_paragraph:
+                text = "</p>%s<p>" % to_unicode(text)
+        return text
+
+
+class Formatter(object):
+    flavor = 'default'
+
+    # Some constants used for clarifying the Wiki regexps:
+
+    BOLDITALIC_TOKEN = "'''''"
+    BOLD_TOKEN = "'''"
+    ITALIC_TOKEN = "''"
+    UNDERLINE_TOKEN = "__"
+    STRIKE_TOKEN = "~~"
+    SUBSCRIPT_TOKEN = ",,"
+    SUPERSCRIPT_TOKEN = r"\^"
+    INLINE_TOKEN = "`"
+    STARTBLOCK_TOKEN = r"\{\{\{"
+    STARTBLOCK = "{{{"
+    ENDBLOCK_TOKEN = r"\}\}\}"
+    ENDBLOCK = "}}}"
+    
+    LINK_SCHEME = r"[\w.+-]+" # as per RFC 2396
+    INTERTRAC_SCHEME = r"[a-zA-Z.+-]*?" # no digits (support for shorthand links)
+
+    QUOTED_STRING = r"'[^']+'|\"[^\"]+\""
+
+    SHREF_TARGET_FIRST = r"[\w/?!#@]"
+    SHREF_TARGET_MIDDLE = r"(?:\|(?=[^|\s])|[^|<>\s])"
+    SHREF_TARGET_LAST = r"[a-zA-Z0-9/=]" # we don't want "_"
+
+    LHREF_RELATIVE_TARGET = r"[/.][^\s[\]]*"
+
+    # Sequence of regexps used by the engine
+
+    _pre_rules = [
+        # Font styles
+        r"(?P<bolditalic>!?%s)" % BOLDITALIC_TOKEN,
+        r"(?P<bold>!?%s)" % BOLD_TOKEN,
+        r"(?P<italic>!?%s)" % ITALIC_TOKEN,
+        r"(?P<underline>!?%s)" % UNDERLINE_TOKEN,
+        r"(?P<strike>!?%s)" % STRIKE_TOKEN,
+        r"(?P<subscript>!?%s)" % SUBSCRIPT_TOKEN,
+        r"(?P<superscript>!?%s)" % SUPERSCRIPT_TOKEN,
+        r"(?P<inlinecode>!?%s(?P<inline>.*?)%s)" \
+        % (STARTBLOCK_TOKEN, ENDBLOCK_TOKEN),
+        r"(?P<inlinecode2>!?%s(?P<inline2>.*?)%s)" \
+        % (INLINE_TOKEN, INLINE_TOKEN)]
+
+    # Rules provided by IWikiSyntaxProviders will be inserted here
+
+    _post_rules = [
+        # > ...
+        r"(?P<citation>^(?P<cdepth>>(?: *>)*))",
+        # &, < and > to &amp;, &lt; and &gt;
+        r"(?P<htmlescape>[&<>])",
+        # wiki:TracLinks
+        r"(?P<shref>!?((?P<sns>%s):(?P<stgt>%s|%s(?:%s*%s)?)))" \
+        % (LINK_SCHEME, QUOTED_STRING,
+           SHREF_TARGET_FIRST, SHREF_TARGET_MIDDLE, SHREF_TARGET_LAST),
+        # [[macro]] call
+        (r"(?P<macro>!?\[\[(?P<macroname>[\w/+-]+)"
+         r"(\]\]|\((?P<macroargs>.*?)\)\]\]))"),
+        # [wiki:TracLinks with label]
+        (r"(?P<lhref>!?\[(?:"
+         r"(?P<rel>%s)|" % LHREF_RELATIVE_TARGET + # ./... or /...
+         r"(?:(?P<lns>%s):)?(?P<ltgt>%s|[^\]\s]*))" % \
+         (LINK_SCHEME, QUOTED_STRING) + # wiki:TracLinks or wiki:"trac links"
+         r"(?:\s+(?P<label>%s|[^\]]+))?\])" % QUOTED_STRING), # label
+        # == heading == #hanchor
+        r"(?P<heading>^\s*(?P<hdepth>=+)\s.*\s(?P=hdepth)\s*"
+        r"(?P<hanchor>#[\w:](?<!\d)[\w:.-]*)?$)",
+        #  * list
+        r"(?P<list>^(?P<ldepth>\s+)(?:[-*]|\d+\.|[a-zA-Z]\.|[ivxIVX]{1,5}\.) )",
+        # definition:: 
+        r"(?P<definition>^\s+((?:%s.*?%s|%s.*?%s|[^%s%s])+?::)(?:\s+|$))"
+        % (INLINE_TOKEN, INLINE_TOKEN, STARTBLOCK_TOKEN, ENDBLOCK_TOKEN,
+           INLINE_TOKEN, STARTBLOCK[0]),
+        # (leading space)
+        r"(?P<indent>^(?P<idepth>\s+)(?=\S))",
+        # || table ||
+        r"(?P<last_table_cell>\|\|\s*$)",
+        r"(?P<table_cell>\|\|)"]
+
+    _processor_re = re.compile('#\!([\w+-][\w+-/]*)')
+    _anchor_re = re.compile('[^\w:.-]+', re.UNICODE)
+
+    def __init__(self, env, req=None, absurls=False, db=None):
+        self.env = env
+        self.req = req
+        self._db = db
+        self._absurls = absurls
+        self._anchors = {}
+        self._open_tags = []
+        self.href = absurls and (req or env).abs_href or (req or env).href
+        self._local = env.config.get('project', 'url') \
+                      or (req or env).abs_href.base
+        self.wiki = WikiSystem(self.env)
+
+    def _get_db(self):
+        if not self._db:
+            self._db = self.env.get_db_cnx()
+        return self._db
+    db = property(fget=_get_db)
+
+    def split_link(self, target):
+        """Split a target along "?" and "#" in `(path, query, fragment)`."""
+        query = fragment = ''
+        idx = target.find('#')
+        if idx >= 0:
+            target, fragment = target[:idx], target[idx:]
+        idx = target.find('?')
+        if idx >= 0:
+            target, query = target[:idx], target[idx:]
+        return (target, query, fragment)
+
+    # -- Pre- IWikiSyntaxProvider rules (Font styles)
+    
+    def tag_open_p(self, tag):
+        """Do we currently have any open tag with `tag` as end-tag?"""
+        return tag in self._open_tags
+
+    def close_tag(self, tag):
+        tmp =  ''
+        for i in xrange(len(self._open_tags)-1, -1, -1):
+            tmp += self._open_tags[i][1]
+            if self._open_tags[i][1] == tag:
+                del self._open_tags[i]
+                for j in xrange(i, len(self._open_tags)):
+                    tmp += self._open_tags[j][0]
+                break
+        return tmp
+
+    def open_tag(self, open, close):
+        self._open_tags.append((open, close))
+
+    def simple_tag_handler(self, match, open_tag, close_tag):
+        """Generic handler for simple binary style tags"""
+        if match[0] == '!':
+            return match[1:]
+        if self.tag_open_p((open_tag, close_tag)):
+            return self.close_tag(close_tag)
+        else:
+            self.open_tag(open_tag, close_tag)
+        return open_tag
+
+    def _bolditalic_formatter(self, match, fullmatch):
+        if match[0] == '!':
+            return match[1:]
+        italic = ('<i>', '</i>')
+        italic_open = self.tag_open_p(italic)
+        tmp = ''
+        if italic_open:
+            tmp += italic[1]
+            self.close_tag(italic[1])
+        tmp += self._bold_formatter(match, fullmatch)
+        if not italic_open:
+            tmp += italic[0]
+            self.open_tag(*italic)
+        return tmp
+
+    def _bold_formatter(self, match, fullmatch):
+        return self.simple_tag_handler(match, '<strong>', '</strong>')
+
+    def _italic_formatter(self, match, fullmatch):
+        return self.simple_tag_handler(match, '<i>', '</i>')
+
+    def _underline_formatter(self, match, fullmatch):
+        return self.simple_tag_handler(match, '<span class="underline">',
+                                       '</span>')
+
+    def _strike_formatter(self, match, fullmatch):
+        return self.simple_tag_handler(match, '<del>', '</del>')
+
+    def _subscript_formatter(self, match, fullmatch):
+        return self.simple_tag_handler(match, '<sub>', '</sub>')
+
+    def _superscript_formatter(self, match, fullmatch):
+        return self.simple_tag_handler(match, '<sup>', '</sup>')
+
+    def _inlinecode_formatter(self, match, fullmatch):
+        return html.TT(fullmatch.group('inline'))
+
+    def _inlinecode2_formatter(self, match, fullmatch):
+        return html.TT(fullmatch.group('inline2'))
+
+    # -- Post- IWikiSyntaxProvider rules
+
+    # HTML escape of &, < and >
+
+    def _htmlescape_formatter(self, match, fullmatch):
+        return match == "&" and "&amp;" or match == "<" and "&lt;" or "&gt;"
+
+    # Short form (shref) and long form (lhref) of TracLinks
+
+    def _unquote(self, text):
+        if text and text[0] in "'\"" and text[0] == text[-1]:
+            return text[1:-1]
+        else:
+            return text
+
+    def _shref_formatter(self, match, fullmatch):
+        ns = fullmatch.group('sns')
+        target = self._unquote(fullmatch.group('stgt'))
+        return self._make_link(ns, target, match, match)
+
+    def _lhref_formatter(self, match, fullmatch):
+        rel = fullmatch.group('rel')
+        ns = fullmatch.group('lns') or (not rel and 'wiki')
+        target = self._unquote(fullmatch.group('ltgt'))
+        label = fullmatch.group('label')
+        if not label: # e.g. `[http://target]` or `[wiki:target]`
+            if target:
+                if target.startswith('//'): # for `[http://target]`
+                    label = ns+':'+target   #  use `http://target`
+                else:                       # for `wiki:target`
+                    label = target          #  use only `target`
+            else: # e.g. `[search:]` 
+                label = ns
+        else:
+            label = self._unquote(label)
+        if rel:
+            return self._make_relative_link(rel, label or rel)
+        else:
+            return self._make_link(ns, target, match, label)
+
+    def _make_link(self, ns, target, match, label):
+        # first check for an alias defined in trac.ini
+        ns = self.env.config.get('intertrac', ns) or ns
+        if ns in self.wiki.link_resolvers:
+            return self.wiki.link_resolvers[ns](self, ns, target,
+                                                escape(label, False))
+        elif target.startswith('//') or ns == "mailto":
+            return self._make_ext_link(ns+':'+target, label)
+        else:
+            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):
+        url = self.env.config.get('intertrac', ns + '.url')
+        if url:
+            name = self.env.config.get('intertrac', ns + '.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) or 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):
+        from trac.wiki.interwiki import InterWikiMap        
+        interwiki = InterWikiMap(self.env)
+        if ns in interwiki:
+            url, title = interwiki.url(ns, target)
+            return self._make_ext_link(url, label, title)
+        else:
+            return None
+
+    def _make_ext_link(self, url, text, title=''):
+        if not url.startswith(self._local):
+            return html.A(html.SPAN(text, class_="icon"),
+                          class_="ext-link", href=url, title=title or None)
+        else:
+            return html.A(text, href=url, title=title or None)
+
+    def _make_relative_link(self, url, text):
+        if url.startswith('//'): # only the protocol will be kept
+            return html.A(text, class_="ext-link", href=url)
+        else:
+            return html.A(text, href=url)
+
+    # WikiMacros
+    
+    def _macro_formatter(self, match, fullmatch):
+        name = fullmatch.group('macroname')
+        if name.lower() == 'br':
+            return '<br />'
+        args = fullmatch.group('macroargs')
+        try:
+            macro = WikiProcessor(self.env, name)
+            return macro.process(self.req, args, True)
+        except Exception, e:
+            self.env.log.error('Macro %s(%s) failed' % (name, args),
+                               exc_info=True)
+            return system_message('Error: Macro %s(%s) failed' % (name, args),
+                                  e)
+
+    # Headings
+
+    def _parse_heading(self, match, fullmatch, shorten):
+        match = match.strip()
+
+        depth = min(len(fullmatch.group('hdepth')), 5)
+        anchor = fullmatch.group('hanchor') or ''
+        heading = match[depth+1:-depth-1-len(anchor)]
+        heading = wiki_to_oneliner(heading, self.env, self.db, shorten,
+                                   self._absurls)
+        if anchor:
+            anchor = anchor[1:]
+        else:
+            sans_markup = heading.plaintext(keeplinebreaks=False)
+            anchor = self._anchor_re.sub('', sans_markup)
+            if not anchor or anchor[0].isdigit() or anchor[0] in '.-':
+                # an ID must start with a Name-start character in XHTML
+                anchor = 'a' + anchor # keeping 'a' for backward compat
+        i = 1
+        anchor_base = anchor
+        while anchor in self._anchors:
+            anchor = anchor_base + str(i)
+            i += 1
+        self._anchors[anchor] = True
+        return (depth, heading, anchor)
+
+    def _heading_formatter(self, match, fullmatch):
+        self.close_table()
+        self.close_paragraph()
+        self.close_indentation()
+        self.close_list()
+        self.close_def_list()
+        depth, heading, anchor = self._parse_heading(match, fullmatch, False)
+        self.out.write('<h%d id="%s">%s</h%d>' %
+                       (depth, anchor, heading, depth))
+
+    # Generic indentation (as defined by lists and quotes)
+
+    def _set_tab(self, depth):
+        """Append a new tab if needed and truncate tabs deeper than `depth`
+
+        given:       -*-----*--*---*--
+        setting:              *
+        results in:  -*-----*-*-------
+        """
+        tabstops = []
+        for ts in self._tabstops:
+            if ts >= depth:
+                break
+            tabstops.append(ts)
+        tabstops.append(depth)
+        self._tabstops = tabstops
+
+    # Lists
+    
+    def _list_formatter(self, match, fullmatch):
+        ldepth = len(fullmatch.group('ldepth'))
+        listid = match[ldepth]
+        self.in_list_item = True
+        class_ = start = None
+        if listid in '-*':
+            type_ = 'ul'
+        else:
+            type_ = 'ol'
+            idx = '01iI'.find(listid)
+            if idx >= 0:
+                class_ = ('arabiczero', None, 'lowerroman', 'upperroman')[idx]
+            elif listid.isdigit():
+                start = match[ldepth:match.find('.')]
+            elif listid.islower():
+                class_ = 'loweralpha'
+            elif listid.isupper():
+                class_ = 'upperalpha'
+        self._set_list_depth(ldepth, type_, class_, start)
+        return ''
+        
+    def _get_list_depth(self):
+        """Return the space offset associated to the deepest opened list."""
+        return self._list_stack and self._list_stack[-1][1] or 0
+
+    def _set_list_depth(self, depth, new_type, list_class, start):
+        def open_list():
+            self.close_table()
+            self.close_paragraph()
+            self.close_indentation() # FIXME: why not lists in quotes?
+            self._list_stack.append((new_type, depth))
+            self._set_tab(depth)
+            class_attr = list_class and ' class="%s"' % list_class or ''
+            start_attr = start and ' start="%s"' % start or ''
+            self.out.write('<'+new_type+class_attr+start_attr+'><li>')
+        def close_list(tp):
+            self._list_stack.pop()
+            self.out.write('</li></%s>' % tp)
+
+        # depending on the indent/dedent, open or close lists
+        if depth > self._get_list_depth():
+            open_list()
+        else:
+            while self._list_stack:
+                deepest_type, deepest_offset = self._list_stack[-1]
+                if depth >= deepest_offset:
+                    break
+                close_list(deepest_type)
+            if depth > 0:
+                if self._list_stack:
+                    old_type, old_offset = self._list_stack[-1]
+                    if new_type and old_type != new_type:
+                        close_list(old_type)
+                        open_list()
+                    else:
+                        if old_offset != depth: # adjust last depth
+                            self._list_stack[-1] = (old_type, depth)
+                        self.out.write('</li><li>')
+                else:
+                    open_list()
+
+    def close_list(self):
+        self._set_list_depth(0, None, None, None)
+
+    # Definition Lists
+
+    def _definition_formatter(self, match, fullmatch):
+        tmp = self.in_def_list and '</dd>' or '<dl>'
+        definition = match[:match.find('::')]
+        tmp += '<dt>%s</dt><dd>' % wiki_to_oneliner(definition, self.env,
+                                                    self.db)
+        self.in_def_list = True
+        return tmp
+
+    def close_def_list(self):
+        if self.in_def_list:
+            self.out.write('</dd></dl>\n')
+        self.in_def_list = False
+
+    # Blockquote
+
+    def _indent_formatter(self, match, fullmatch):
+        idepth = len(fullmatch.group('idepth'))
+        if self._list_stack:
+            ltype, ldepth = self._list_stack[-1]
+            if idepth < ldepth:
+                for _, ldepth in self._list_stack:
+                    if idepth > ldepth:
+                        self.in_list_item = True
+                        self._set_list_depth(idepth, None, None, None)
+                        return ''
+            elif idepth <= ldepth + (ltype == 'ol' and 3 or 2):
+                self.in_list_item = True
+                return ''
+        if not self.in_def_list:
+            self._set_quote_depth(idepth)
+        return ''
+
+    def _citation_formatter(self, match, fullmatch):
+        cdepth = len(fullmatch.group('cdepth').replace(' ', ''))
+        self._set_quote_depth(cdepth, True)
+        return ''
+
+    def close_indentation(self):
+        self._set_quote_depth(0)
+
+    def _get_quote_depth(self):
+        """Return the space offset associated to the deepest opened quote."""
+        return self._quote_stack and self._quote_stack[-1] or 0
+
+    def _set_quote_depth(self, depth, citation=False):
+        def open_quote(depth):
+            self.close_table()
+            self.close_paragraph()
+            self.close_list()
+            def open_one_quote(d):
+                self._quote_stack.append(d)
+                self._set_tab(d)
+                class_attr = citation and ' class="citation"' or ''
+                self.out.write('<blockquote%s>' % class_attr + os.linesep)
+            if citation:
+                for d in range(quote_depth+1, depth+1):
+                    open_one_quote(d)
+            else:
+                open_one_quote(depth)
+        def close_quote():
+            self.close_table()
+            self.close_paragraph()
+            self._quote_stack.pop()
+            self.out.write('</blockquote>' + os.linesep)
+        quote_depth = self._get_quote_depth()
+        if depth > quote_depth:
+            self._set_tab(depth)
+            tabstops = self._tabstops[::-1]
+            while tabstops:
+                tab = tabstops.pop()
+                if tab > quote_depth:
+                    open_quote(tab)
+        else:
+            while self._quote_stack:
+                deepest_offset = self._quote_stack[-1]
+                if depth >= deepest_offset:
+                    break
+                close_quote()
+            if not citation and depth > 0:
+                if self._quote_stack:
+                    old_offset = self._quote_stack[-1]
+                    if old_offset != depth: # adjust last depth
+                        self._quote_stack[-1] = depth
+                else:
+                    open_quote(depth)
+        if depth > 0:
+            self.in_quote = True
+
+    # Table
+    
+    def _last_table_cell_formatter(self, match, fullmatch):
+        return ''
+
+    def _table_cell_formatter(self, match, fullmatch):
+        self.open_table()
+        self.open_table_row()
+        if self.in_table_cell:
+            return '</td><td>'
+        else:
+            self.in_table_cell = 1
+            return '<td>'
+
+    def open_table(self):
+        if not self.in_table:
+            self.close_paragraph()
+            self.close_list()
+            self.close_def_list()
+            self.in_table = 1
+            self.out.write('<table class="wiki">' + os.linesep)
+
+    def open_table_row(self):
+        if not self.in_table_row:
+            self.open_table()
+            self.in_table_row = 1
+            self.out.write('<tr>')
+
+    def close_table_row(self):
+        if self.in_table_row:
+            self.in_table_row = 0
+            if self.in_table_cell:
+                self.in_table_cell = 0
+                self.out.write('</td>')
+
+            self.out.write('</tr>')
+
+    def close_table(self):
+        if self.in_table:
+            self.close_table_row()
+            self.out.write('</table>' + os.linesep)
+            self.in_table = 0
+
+    # Paragraphs
+
+    def open_paragraph(self):
+        if not self.paragraph_open:
+            self.out.write('<p>' + os.linesep)
+            self.paragraph_open = 1
+
+    def close_paragraph(self):
+        if self.paragraph_open:
+            while self._open_tags != []:
+                self.out.write(self._open_tags.pop()[1])
+            self.out.write('</p>' + os.linesep)
+            self.paragraph_open = 0
+
+    # Code blocks
+    
+    def handle_code_block(self, line):
+        if line.strip() == Formatter.STARTBLOCK:
+            self.in_code_block += 1
+            if self.in_code_block == 1:
+                self.code_processor = None
+                self.code_text = ''
+            else:
+                self.code_text += line + os.linesep
+                if not self.code_processor:
+                    self.code_processor = WikiProcessor(self.env, 'default')
+        elif line.strip() == Formatter.ENDBLOCK:
+            self.in_code_block -= 1
+            if self.in_code_block == 0 and self.code_processor:
+                self.close_table()
+                self.close_paragraph()
+                self.out.write(to_unicode(self.code_processor.process(
+                    self.req, self.code_text)))
+            else:
+                self.code_text += line + os.linesep
+        elif not self.code_processor:
+            match = Formatter._processor_re.search(line)
+            if match:
+                name = match.group(1)
+                self.code_processor = WikiProcessor(self.env, name)
+            else:
+                self.code_text += line + os.linesep 
+                self.code_processor = WikiProcessor(self.env, 'default')
+        else:
+            self.code_text += line + os.linesep
+
+    def close_code_blocks(self):
+        while self.in_code_block > 0:
+            self.handle_code_block(Formatter.ENDBLOCK)
+
+    # -- Wiki engine
+    
+    def handle_match(self, fullmatch):
+        for itype, match in fullmatch.groupdict().items():
+            if match and not itype in self.wiki.helper_patterns:
+                # Check for preceding escape character '!'
+                if match[0] == '!':
+                    return escape(match[1:])
+                if itype in self.wiki.external_handlers:
+                    external_handler = self.wiki.external_handlers[itype]
+                    return external_handler(self, match, fullmatch)
+                else:
+                    internal_handler = getattr(self, '_%s_formatter' % itype)
+                    return internal_handler(match, fullmatch)
+
+    def replace(self, fullmatch):
+        """Replace one match with its corresponding expansion"""
+        replacement = self.handle_match(fullmatch)
+        if replacement:
+            return to_unicode(replacement)
+
+    def reset(self, out=None):
+        class NullOut(object):
+            def write(self, data): pass
+        self.out = out or NullOut()
+        self._open_tags = []
+        self._list_stack = []
+        self._quote_stack = []
+        self._tabstops = []
+
+        self.in_code_block = 0
+        self.in_table = 0
+        self.in_def_list = 0
+        self.in_table_row = 0
+        self.in_table_cell = 0
+        self.paragraph_open = 0
+
+    def format(self, text, out=None, escape_newlines=False):
+        self.reset(out)
+        for line in text.splitlines():
+            # Handle code block
+            if self.in_code_block or line.strip() == Formatter.STARTBLOCK:
+                self.handle_code_block(line)
+                continue
+            # Handle Horizontal ruler
+            elif line[0:4] == '----':
+                self.close_table()
+                self.close_paragraph()
+                self.close_indentation()
+                self.close_list()
+                self.close_def_list()
+                self.out.write('<hr />' + os.linesep)
+                continue
+            # Handle new paragraph
+            elif line == '':
+                self.close_paragraph()
+                self.close_indentation()
+                self.close_list()
+                self.close_def_list()
+                continue
+
+            # Tab expansion and clear tabstops if no indent
+            line = line.replace('\t', ' '*8)
+            if not line.startswith(' '):
+                self._tabstops = []
+
+            if escape_newlines:
+                line += ' [[BR]]'
+            self.in_list_item = False
+            self.in_quote = False
+            # Throw a bunch of regexps on the problem
+            result = re.sub(self.wiki.rules, self.replace, line)
+
+            if not self.in_list_item:
+                self.close_list()
+
+            if not self.in_quote:
+                self.close_indentation()
+
+            if self.in_def_list and not line.startswith(' '):
+                self.close_def_list()
+
+            if self.in_table and line.strip()[0:2] != '||':
+                self.close_table()
+
+            if len(result) and not self.in_list_item and not self.in_def_list \
+                    and not self.in_table:
+                self.open_paragraph()
+            self.out.write(result + os.linesep)
+            self.close_table_row()
+
+        self.close_table()
+        self.close_paragraph()
+        self.close_indentation()
+        self.close_list()
+        self.close_def_list()
+        self.close_code_blocks()
+
+
+class OneLinerFormatter(Formatter):
+    """
+    A special version of the wiki formatter that only implement a
+    subset of the wiki formatting functions. This version is useful
+    for rendering short wiki-formatted messages on a single line
+    """
+    flavor = 'oneliner'
+
+    def __init__(self, env, absurls=False, db=None):
+        Formatter.__init__(self, env, None, absurls, db)
+
+    # Override a few formatters to disable some wiki syntax in "oneliner"-mode
+    def _list_formatter(self, match, fullmatch): return match
+    def _indent_formatter(self, match, fullmatch): return match
+    def _citation_formatter(self, match, fullmatch):
+        return escape(match, False)
+    def _heading_formatter(self, match, fullmatch):
+        return escape(match, False)
+    def _definition_formatter(self, match, fullmatch):
+        return escape(match, False)
+    def _table_cell_formatter(self, match, fullmatch): return match
+    def _last_table_cell_formatter(self, match, fullmatch): return match
+
+    def _macro_formatter(self, match, fullmatch):
+        name = fullmatch.group('macroname')
+        if name.lower() == 'br':
+            return ' '
+        elif name == 'comment':
+            return ''
+        else:
+            args = fullmatch.group('macroargs')
+            return '[[%s%s]]' % (name,  args and '(...)' or '')
+
+    def format(self, text, out, shorten=False):
+        if not text:
+            return
+        self.out = out
+        self._open_tags = []
+
+        # Simplify code blocks
+        in_code_block = 0
+        processor = None
+        buf = StringIO()
+        for line in text.strip().splitlines():
+            if line.strip() == Formatter.STARTBLOCK:
+                in_code_block += 1
+            elif line.strip() == Formatter.ENDBLOCK:
+                if in_code_block:
+                    in_code_block -= 1
+                    if in_code_block == 0:
+                        if processor != 'comment':
+                            buf.write(' ![...]' + os.linesep)
+                        processor = None
+            elif in_code_block:
+                if not processor:
+                    if line.startswith('#!'):
+                        processor = line[2:].strip()
+            else:
+                buf.write(line + os.linesep)
+        result = buf.getvalue()[:-1]
+
+        if shorten:
+            result = shorten_line(result)
+
+        result = re.sub(self.wiki.rules, self.replace, result)
+        result = result.replace('[...]', '[&hellip;]')
+        if result.endswith('...'):
+            result = result[:-3] + '&hellip;'
+
+        # Close all open 'one line'-tags
+        result += self.close_tag(None)
+        # Flush unterminated code blocks
+        if in_code_block > 0:
+            result += '[&hellip;]'
+        out.write(result)
+
+
+class OutlineFormatter(Formatter):
+    """Special formatter that generates an outline of all the headings."""
+    flavor = 'outline'
+    
+    def __init__(self, env, absurls=False, db=None):
+        Formatter.__init__(self, env, None, absurls, db)
+
+    # Avoid the possible side-effects of rendering WikiProcessors
+
+    def _macro_formatter(self, match, fullmatch):
+        return ''
+
+    def handle_code_block(self, line):
+        if line.strip() == Formatter.STARTBLOCK:
+            self.in_code_block += 1
+        elif line.strip() == Formatter.ENDBLOCK:
+            self.in_code_block -= 1
+
+    def format(self, text, out, max_depth=6, min_depth=1):
+        self.outline = []
+        Formatter.format(self, text)
+
+        if min_depth > max_depth:
+            min_depth, max_depth = max_depth, min_depth
+        max_depth = min(6, max_depth)
+        min_depth = max(1, min_depth)
+
+        curr_depth = min_depth - 1
+        for depth, anchor, text in self.outline:
+            if depth < min_depth or depth > max_depth:
+                continue
+            if depth < curr_depth:
+                out.write('</li></ol><li>' * (curr_depth - depth))
+            elif depth > curr_depth:
+                out.write('<ol><li>' * (depth - curr_depth))
+            else:
+                out.write("</li><li>\n")
+            curr_depth = depth
+            out.write('<a href="#%s">%s</a>' % (anchor, text))
+        out.write('</li></ol>' * curr_depth)
+
+    def _heading_formatter(self, match, fullmatch):
+        depth, heading, anchor = self._parse_heading(match, fullmatch, True)
+        heading = re.sub(r'</?a(?: .*?)?>', '', heading) # Strip out link tags
+        self.outline.append((depth, anchor, heading))
+
+
+class LinkFormatter(OutlineFormatter):
+    """Special formatter that focuses on TracLinks."""
+    flavor = 'outline'
+    
+    def __init__(self, env, absurls=False, db=None):
+        OutlineFormatter.__init__(self, env, absurls, db)
+        
+    def match(self, wikitext):
+        """Return the Wiki match found at the beginning of the `wikitext`"""
+        self.reset()        
+        match = re.match(self.wiki.rules, wikitext)
+        if match:
+            return self.handle_match(match)
+
+
+# -- wiki_to_* helper functions
+
+def wiki_to_html(wikitext, env, req, db=None,
+                 absurls=False, escape_newlines=False):
+    if not wikitext:
+        return Markup()
+    out = StringIO()
+    Formatter(env, req, absurls, db).format(wikitext, out, escape_newlines)
+    return Markup(out.getvalue())
+
+def wiki_to_oneliner(wikitext, env, db=None, shorten=False, absurls=False):
+    if not wikitext:
+        return Markup()
+    out = StringIO()
+    OneLinerFormatter(env, absurls, db).format(wikitext, out, shorten)
+    return Markup(out.getvalue())
+
+def wiki_to_outline(wikitext, env, db=None,
+                    absurls=False, max_depth=None, min_depth=None):
+    if not wikitext:
+        return Markup()
+    out = StringIO()
+    OutlineFormatter(env, absurls, db).format(wikitext, out, max_depth,
+                                              min_depth)
+    return Markup(out.getvalue())
+
+def wiki_to_link(wikitext, env, req):
+    if not wikitext:
+        return ''
+    return LinkFormatter(env, False, None).match(wikitext)
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/wiki/interwiki.py
@@ -0,0 +1,146 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2006 Edgewall Software
+# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christian Boos <cboos@neuf.fr>
+
+import re
+
+from trac.core import *
+from trac.wiki.formatter import Formatter
+from trac.wiki.api import IWikiChangeListener, IWikiMacroProvider
+
+class InterWikiMap(Component):
+    """Implements support for InterWiki maps."""
+
+    implements(IWikiChangeListener, IWikiMacroProvider)
+
+    _page_name = 'InterMapTxt'
+    _interwiki_re = re.compile(r"(%s)[ \t]+([^ \t]+)(?:[ \t]+#(.*))?" %
+                               Formatter.LINK_SCHEME, 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;
+
+    # The component itself behaves as a map
+
+    def __contains__(self, ns):
+        self._update()
+        return ns.upper() in self._interwiki_map
+
+    def __getitem__(self, ns):
+        self._update()
+        return self._interwiki_map[ns.upper()]
+
+    def __setitem__(self, ns, value):
+        self._update()
+        self._interwiki_map[ns.upper()] = value
+
+    def keys(self):
+        self._update()
+        return self._interwiki_map.keys()
+
+    # Expansion of positional arguments ($1, $2, ...) in URL and title
+    def _expand(self, txt, args):
+        """Replace "$1" by the first args, "$2" by the second, etc."""
+        def setarg(match):
+            num = int(match.group()[1:])
+            return 0 < num <= len(args) and args[num-1] or ''
+        return re.sub(InterWikiMap._argspec_re, setarg, txt)
+
+    def _expand_or_append(self, txt, args):
+        """Like expand, but also append first arg if there's no "$"."""
+        if not args:
+            return txt
+        expanded = self._expand(txt, args)
+        return expanded == txt and txt + args[0] or expanded
+
+    def url(self, ns, target):
+        """Return `(url, title)` for the given InterWiki `ns`.
+        
+        Expand the colon-separated `target` arguments.
+        """
+        ns, url, title = self[ns]
+        args = target.split(':')
+        expanded_url = self._expand_or_append(url, args)
+        expanded_title = self._expand(title, args)
+        if expanded_title == title:
+            expanded_title = target+' in '+title
+        return expanded_url, expanded_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 = None
+
+    def wiki_page_version_deleted(self, page):
+        if page.name == InterWikiMap._page_name:
+            self._update()
+
+    def _update(self):
+        from trac.wiki.model import WikiPage
+        if self._interwiki_map is not None:
+            return
+        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[prefix] = (prefix, url, title)
+            elif line.startswith('----'):
+                in_map = True
+
+    # IWikiMacroProvider methods
+
+    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):
+        from trac.util import sorted
+        from trac.util.markup import html as _
+        interwikis = []
+        for k in sorted(self.keys()):
+            prefix, url, title = self[k]
+            interwikis.append({
+                'prefix': prefix, 'url': url, 'title': title,
+                'rc_url': self._expand_or_append(url, ['RecentChanges']),
+                'description': title == prefix and url or title})
+
+        return _.TABLE(_.TR(_.TH(_.EM("Prefix")), _.TH(_.EM("Site"))),
+                       [ _.TR(_.TD(_.A(w['prefix'], href=w['rc_url'])),
+                              _.TD(_.A(w['description'], href=w['url'])))
+                         for w in interwikis ],
+                       class_="wiki interwiki")
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/wiki/macros.py
@@ -0,0 +1,491 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2005-2006 Edgewall Software
+# Copyright (C) 2005-2006 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Christopher Lenz <cmlenz@gmx.de>
+
+import imp
+import inspect
+import os
+import re
+try:
+    set
+except NameError:
+    from sets import Set as set
+from StringIO import StringIO
+
+from trac.config import default_dir
+from trac.core import *
+from trac.util import sorted
+from trac.util.datefmt import format_date
+from trac.util.markup import escape, html, Markup
+from trac.wiki.api import IWikiMacroProvider, WikiSystem
+from trac.wiki.model import WikiPage
+from trac.web.chrome import add_stylesheet
+
+
+class WikiMacroBase(Component):
+    """Abstract base class for wiki macros."""
+
+    implements(IWikiMacroProvider)
+    abstract = True
+
+    def get_macros(self):
+        """Yield the name of the macro based on the class name."""
+        name = self.__class__.__name__
+        if name.endswith('Macro'):
+            name = name[:-5]
+        yield name
+
+    def get_macro_description(self, name):
+        """Return the subclass's docstring."""
+        return inspect.getdoc(self.__class__)
+
+    def render_macro(self, req, name, content):
+        raise NotImplementedError
+
+
+class TitleIndexMacro(WikiMacroBase):
+    """Inserts an alphabetic list of all wiki pages into the output.
+
+    Accepts a prefix string as parameter: if provided, only pages with names
+    that start with the prefix are included in the resulting list. If this
+    parameter is omitted, all pages are listed.
+    """
+
+    def render_macro(self, req, name, content):
+        prefix = content or None
+
+        wiki = WikiSystem(self.env)
+
+        return html.UL([html.LI(html.A(wiki.format_page_name(page),
+                                       href=req.href.wiki(page)))
+                        for page in sorted(wiki.get_pages(prefix))])
+
+
+class RecentChangesMacro(WikiMacroBase):
+    """Lists all pages that have recently been modified, grouping them by the
+    day they were last modified.
+
+    This macro accepts two parameters. The first is a prefix string: if
+    provided, only pages with names that start with the prefix are included in
+    the resulting list. If this parameter is omitted, all pages are listed.
+
+    The second parameter is a number for limiting the number of pages returned.
+    For example, specifying a limit of 5 will result in only the five most
+    recently changed pages to be included in the list.
+    """
+
+    def render_macro(self, req, name, content):
+        prefix = limit = None
+        if content:
+            argv = [arg.strip() for arg in content.split(',')]
+            if len(argv) > 0:
+                prefix = argv[0]
+                if len(argv) > 1:
+                    limit = int(argv[1])
+
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+
+        sql = 'SELECT name, ' \
+              '  max(version) AS max_version, ' \
+              '  max(time) AS max_time ' \
+              'FROM wiki'
+        args = []
+        if prefix:
+            sql += ' WHERE name LIKE %s'
+            args.append(prefix + '%')
+        sql += ' GROUP BY name ORDER BY max_time DESC'
+        if limit:
+            sql += ' LIMIT %s'
+            args.append(limit)
+        cursor.execute(sql, args)
+
+        entries_per_date = []
+        prevdate = None
+        for name, version, time in cursor:
+            date = format_date(time)
+            if date != prevdate:
+                prevdate = date
+                entries_per_date.append((date, []))
+            entries_per_date[-1][1].append((name, version))
+
+        wiki = WikiSystem(self.env)
+        return html.DIV(
+            [html.H3(date) +
+             html.UL([html.LI(
+            html.A(wiki.format_page_name(name), href=req.href.wiki(name)), ' ',
+            html.SMALL('(', html.A('diff',
+                                   href=req.href.wiki(name, action='diff',
+                                                      version=version)), ')'))
+                      for name, version in entries])
+             for date, entries in entries_per_date])
+
+
+class PageOutlineMacro(WikiMacroBase):
+    """Displays a structural outline of the current wiki page, each item in the
+    outline being a link to the corresponding heading.
+
+    This macro accepts three optional parameters:
+    
+     * The first is a number or range that allows configuring the minimum and
+       maximum level of headings that should be included in the outline. For
+       example, specifying "1" here will result in only the top-level headings
+       being included in the outline. Specifying "2-3" will make the outline
+       include all headings of level 2 and 3, as a nested list. The default is
+       to include all heading levels.
+     * The second parameter can be used to specify a custom title (the default
+       is no title).
+     * The third parameter selects the style of the outline. This can be
+       either `inline` or `pullout` (the latter being the default). The `inline`
+       style renders the outline as normal part of the content, while `pullout`
+       causes the outline to be rendered in a box that is by default floated to
+       the right side of the other content.
+    """
+
+    def render_macro(self, req, name, content):
+        from trac.wiki.formatter import wiki_to_outline
+        min_depth, max_depth = 1, 6
+        title = None
+        inline = 0
+        if content:
+            argv = [arg.strip() for arg in content.split(',')]
+            if len(argv) > 0:
+                depth = argv[0]
+                if depth.find('-') >= 0:
+                    min_depth, max_depth = [int(d) for d in depth.split('-', 1)]
+                else:
+                    min_depth, max_depth = int(depth), int(depth)
+                if len(argv) > 1:
+                    title = argv[1].strip()
+                    if len(argv) > 2:
+                        inline = argv[2].strip().lower() == 'inline'
+
+        db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        pagename = req.args.get('page') or 'WikiStart'
+        page = WikiPage(self.env, pagename)
+
+        buf = StringIO()
+        if not inline:
+            buf.write('<div class="wiki-toc">')
+        if title:
+            buf.write('<h4>%s</h4>' % escape(title))
+        buf.write(wiki_to_outline(page.text, self.env, db=db,
+                                  max_depth=max_depth, min_depth=min_depth))
+        if not inline:
+            buf.write('</div>')
+        return buf.getvalue()
+
+
+class ImageMacro(WikiMacroBase):
+    """Embed an image in wiki-formatted text.
+    
+    The first argument is the file specification. The file specification may
+    reference attachments or files in three ways:
+     * `module:id:file`, where module can be either '''wiki''' or '''ticket''',
+       to refer to the attachment named ''file'' of the specified wiki page or
+       ticket.
+     * `id:file`: same as above, but id is either a ticket shorthand or a Wiki
+       page name.
+     * `file` to refer to a local attachment named 'file'. This only works from
+       within that wiki page or a ticket.
+    
+    Also, the file specification may refer to repository files, using the
+    `source:file` syntax (`source:file@rev` works also).
+    
+    The remaining arguments are optional and allow configuring the attributes
+    and style of the rendered `<img>` element:
+     * digits and unit are interpreted as the size (ex. 120, 25%)
+       for the image
+     * `right`, `left`, `top` or `bottom` are interpreted as the alignment for
+       the image
+     * `nolink` means without link to image source.
+     * `key=value` style are interpreted as HTML attributes or CSS style
+        indications for the image. Valid keys are:
+        * align, border, width, height, alt, title, longdesc, class, id
+          and usemap
+        * `border` can only be a number
+    
+    Examples:
+    {{{
+        [[Image(photo.jpg)]]                           # simplest
+        [[Image(photo.jpg, 120px)]]                    # with size
+        [[Image(photo.jpg, right)]]                    # aligned by keyword
+        [[Image(photo.jpg, nolink)]]                   # without link to source
+        [[Image(photo.jpg, align=right)]]              # aligned by attribute
+    }}}
+    
+    You can use image from other page, other ticket or other module.
+    {{{
+        [[Image(OtherPage:foo.bmp)]]    # if current module is wiki
+        [[Image(base/sub:bar.bmp)]]     # from hierarchical wiki page
+        [[Image(#3:baz.bmp)]]           # if in a ticket, point to #3
+        [[Image(ticket:36:boo.jpg)]]
+        [[Image(source:/images/bee.jpg)]] # straight from the repository!
+        [[Image(htdocs:foo/bar.png)]]   # image file in project htdocs dir.
+    }}}
+    
+    ''Adapted from the Image.py macro created by Shun-ichi Goto
+    <gotoh@taiyo.co.jp>''
+    """
+
+    def render_macro(self, req, name, content):
+        # args will be null if the macro is called without parenthesis.
+        if not content:
+            return ''
+        # parse arguments
+        # we expect the 1st argument to be a filename (filespec)
+        args = content.split(',')
+        if len(args) == 0:
+            raise Exception("No argument.")
+        filespec = args[0]
+        size_re = re.compile('[0-9]+%?$')
+        attr_re = re.compile('(align|border|width|height|alt'
+                             '|title|longdesc|class|id|usemap)=(.+)')
+        quoted_re = re.compile("(?:[\"'])(.*)(?:[\"'])$")
+        attr = {}
+        style = {}
+        nolink = False
+        for arg in args[1:]:
+            arg = arg.strip()
+            if size_re.match(arg):
+                # 'width' keyword
+                attr['width'] = arg
+                continue
+            if arg == 'nolink':
+                nolink = True
+                continue
+            match = attr_re.match(arg)
+            if match:
+                key, val = match.groups()
+                m = quoted_re.search(val) # unquote "..." and '...'
+                if m:
+                    val = m.group(1)
+                if key == 'align':
+                    style['float'] = val
+                elif key == 'border':
+                    style['border'] = ' %dpx solid' % int(val);
+                else:
+                    attr[str(key)] = val # will be used as a __call__ keyword
+
+        # parse filespec argument to get module and id if contained.
+        parts = filespec.split(':')
+        url = None
+        if len(parts) == 3:                 # module:id:attachment
+            if parts[0] in ['wiki', 'ticket']:
+                module, id, file = parts
+            else:
+                raise Exception("%s module can't have attachments" % parts[0])
+        elif len(parts) == 2:
+            from trac.versioncontrol.web_ui import BrowserModule
+            try:
+                browser_links = [link for link,_ in 
+                                 BrowserModule(self.env).get_link_resolvers()]
+            except Exception:
+                browser_links = []
+            if parts[0] in browser_links:   # source:path
+                module, file = parts
+                rev = None
+                if '@' in file:
+                    file, rev = file.split('@')
+                url = req.href.browser(file, rev=rev)
+                raw_url = req.href.browser(file, rev=rev, format='raw')
+                desc = filespec
+            else: # #ticket:attachment or WikiPage:attachment
+                # FIXME: do something generic about shorthand forms...
+                id, file = parts
+                if id and id[0] == '#':
+                    module = 'ticket'
+                    id = id[1:]
+                elif id == 'htdocs':
+                    raw_url = url = req.href.chrome('site', file)
+                    desc = os.path.basename(file)
+                else:
+                    module = 'wiki'
+        elif len(parts) == 1:               # attachment
+            # determine current object
+            # FIXME: should be retrieved from the formatter...
+            # ...and the formatter should be provided to the macro
+            file = filespec
+            module, id = 'wiki', 'WikiStart'
+            path_info = req.path_info.split('/',2)
+            if len(path_info) > 1:
+                module = path_info[1]
+            if len(path_info) > 2:
+                id = path_info[2]
+            if module not in ['wiki', 'ticket']:
+                raise Exception('Cannot reference local attachment from here')
+        else:
+            raise Exception('No filespec given')
+        if not url: # this is an attachment
+            from trac.attachment import Attachment
+            attachment = Attachment(self.env, module, id, file)
+            url = attachment.href(req)
+            raw_url = attachment.href(req, format='raw')
+            desc = attachment.description
+        for key in ['title', 'alt']:
+            if desc and not attr.has_key(key):
+                attr[key] = desc
+        if style:
+            attr['style'] = '; '.join(['%s:%s' % (k, escape(v))
+                                       for k, v in style.iteritems()])
+        result = Markup(html.IMG(src=raw_url, **attr)).sanitize()
+        if not nolink:
+            result = html.A(result, href=url, style='padding:0; border:none')
+        return result
+
+
+class MacroListMacro(WikiMacroBase):
+    """Displays a list of all installed Wiki macros, including documentation if
+    available.
+    
+    Optionally, the name of a specific macro can be provided as an argument. In
+    that case, only the documentation for that macro will be rendered.
+    
+    Note that this macro will not be able to display the documentation of
+    macros if the `PythonOptimize` option is enabled for mod_python!
+    """
+
+    def render_macro(self, req, name, content):
+        from trac.wiki.formatter import wiki_to_html, system_message
+        wiki = WikiSystem(self.env)
+
+        def get_macro_descr():
+            for macro_provider in wiki.macro_providers:
+                for macro_name in macro_provider.get_macros():
+                    if content and macro_name != content:
+                        continue
+                    try:
+                        descr = macro_provider.get_macro_description(macro_name)
+                        descr = wiki_to_html(descr or '', self.env, req)
+                    except Exception, e:
+                        descr = Markup(system_message(
+                            "Error: Can't get description for macro %s" \
+                            % macro_name, e))
+                    yield (macro_name, descr)
+
+        return html.DL([(html.DT(html.CODE('[[',macro_name,']]')),
+                         html.DD(description))
+                        for macro_name, description in get_macro_descr()])
+
+
+class InterTracMacro(WikiMacroBase):
+    """Provide a list of known InterTrac prefixes."""
+
+    def render_macro(self, req, name, content):
+        intertracs = {}
+        for key, value in self.config.options('intertrac'):
+            if '.' in key:
+                prefix, attribute = key.split('.', 1)
+                intertrac = intertracs.setdefault(prefix, {})
+                intertrac[attribute] = value
+            else:
+                intertracs[key] = value # alias
+
+        def generate_prefix(prefix):
+            intertrac = intertracs[prefix]
+            if isinstance(intertrac, basestring):
+                yield html.TR(html.TD(html.B(prefix)),
+                              html.TD('Alias for ', html.B(intertrac)))
+            else:
+                url = intertrac.get('url', '')
+                if url:
+                    title = intertrac.get('title', url)
+                    yield html.TR(html.TD(html.A(html.B(prefix),
+                                                 href=url + '/timeline')),
+                                  html.TD(html.A(title, href=url)))
+
+        return html.TABLE(class_="wiki intertrac")(
+            html.TR(html.TH(html.EM('Prefix')), html.TH(html.EM('Trac Site'))),
+            [generate_prefix(p) for p in sorted(intertracs.keys())])
+
+
+class TracIniMacro(WikiMacroBase):
+    """Produce documentation for Trac configuration file.
+
+    Typically, this will be used in the TracIni page.
+    Optional arguments are a configuration section filter,
+    and a configuration option name filter: only the configuration
+    options whose section and name start with the filters are output.
+    """
+
+    def render_macro(self, req, name, filter):
+        from trac.config import Option
+        from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner
+        filter = filter or ''
+
+        sections = set([section for section, option in Option.registry.keys()
+                        if section.startswith(filter)])
+
+        return html.DIV(class_='tracini')(
+            [(html.H2('[%s]' % section, id='%s-section' % section),
+              html.TABLE(class_='wiki')(
+                  html.TBODY([html.TR(html.TD(html.TT(option.name)),
+                                      html.TD(wiki_to_oneliner(option.__doc__,
+                                                               self.env)))
+                              for option in Option.registry.values()
+                              if option.section == section])))
+             for section in sorted(sections)])
+
+
+class UserMacroProvider(Component):
+    """Adds macros that are provided as Python source files in the
+    `wiki-macros` directory of the environment, or the global macros
+    directory.
+    """
+    implements(IWikiMacroProvider)
+
+    def __init__(self):
+        self.env_macros = os.path.join(self.env.path, 'wiki-macros')
+        self.site_macros = default_dir('macros')
+
+    # IWikiMacroProvider methods
+
+    def get_macros(self):
+        found = []
+        for path in (self.env_macros, self.site_macros):
+            if not os.path.exists(path):
+                continue
+            for filename in [filename for filename in os.listdir(path)
+                             if filename.lower().endswith('.py')
+                             and not filename.startswith('__')]:
+                try:
+                    module = self._load_macro(filename[:-3])
+                    name = module.__name__
+                    if name in found:
+                        continue
+                    found.append(name)
+                    yield name
+                except Exception, e:
+                    self.log.error('Failed to load wiki macro %s (%s)',
+                                   filename, e, exc_info=True)
+
+    def get_macro_description(self, name):
+        return inspect.getdoc(self._load_macro(name))
+
+    def render_macro(self, req, name, content):
+        module = self._load_macro(name)
+        try:
+            return module.execute(req and req.hdf, content, self.env)
+        except Exception, e:
+            self.log.error('Wiki macro %s failed (%s)', name, e, exc_info=True)
+            raise
+
+    def _load_macro(self, name):
+        for path in (self.env_macros, self.site_macros):
+            macro_file = os.path.join(path, name + '.py')
+            if os.path.isfile(macro_file):
+                return imp.load_source(name, macro_file)
+        raise TracError, 'Macro %s not found' % name
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/wiki/model.py
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2005 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+import time
+
+from trac.core import *
+from trac.wiki.api import WikiSystem
+
+
+class WikiPage(object):
+    """Represents a wiki page (new or existing)."""
+
+    def __init__(self, env, name=None, version=None, db=None):
+        self.env = env
+        self.name = name
+        if name:
+            self._fetch(name, version, db)
+        else:
+            self.version = 0
+            self.text = ''
+            self.readonly = 0
+        self.old_text = self.text
+        self.old_readonly = self.readonly
+
+    def _fetch(self, name, version=None, db=None):
+        if not db:
+            db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        if version:
+            cursor.execute("SELECT version,time,author,text,comment,readonly "
+                           "FROM wiki "
+                           "WHERE name=%s AND version=%s",
+                           (name, int(version)))
+        else:
+            cursor.execute("SELECT version,time,author,text,comment,readonly "
+                           "FROM wiki "
+                           "WHERE name=%s ORDER BY version DESC LIMIT 1",
+                           (name,))
+        row = cursor.fetchone()
+        if row:
+            version,time,author,text,comment,readonly = row
+            self.version = int(version)
+            self.author = author
+            self.time = time
+            self.text = text
+            self.comment = comment
+            self.readonly = readonly and int(readonly) or 0
+        else:
+            self.version = 0
+            self.text = self.comment = self.author = ''
+            self.time = self.readonly = 0
+
+    exists = property(fget=lambda self: self.version > 0)
+
+    def delete(self, version=None, db=None):
+        assert self.exists, 'Cannot delete non-existent page'
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        page_deleted = False
+        cursor = db.cursor()
+        if version is None:
+            # Delete a wiki page completely
+            cursor.execute("DELETE FROM wiki WHERE name=%s", (self.name,))
+            self.env.log.info('Deleted page %s' % self.name)
+        else:
+            # Delete only a specific page version
+            cursor.execute("DELETE FROM wiki WHERE name=%s and version=%s",
+                           (self.name, version))
+            self.env.log.info('Deleted version %d of page %s'
+                              % (version, self.name))
+
+        if version is None or version == self.version:
+            self._fetch(self.name, None, db)
+
+        if not self.exists:
+            from trac.attachment import Attachment
+            # Delete orphaned attachments
+            for attachment in Attachment.select(self.env, 'wiki', self.name, db):
+                attachment.delete(db)
+
+        if handle_ta:
+            db.commit()
+
+        # Let change listeners know about the deletion
+        if not self.exists:
+            for listener in WikiSystem(self.env).change_listeners:
+                listener.wiki_page_deleted(self)
+        else:
+            for listener in WikiSystem(self.env).change_listeners:
+                if hasattr(listener, 'wiki_page_version_deleted'):
+                    listener.wiki_page_version_deleted(self)
+
+
+    def save(self, author, comment, remote_addr, t=None, db=None):
+        if not db:
+            db = self.env.get_db_cnx()
+            handle_ta = True
+        else:
+            handle_ta = False
+
+        if t is None:
+            t = time.time()
+
+        if self.text != self.old_text:
+            cursor = db.cursor()
+            cursor.execute("INSERT INTO wiki (name,version,time,author,ipnr,"
+                           "text,comment,readonly) VALUES (%s,%s,%s,%s,%s,%s,"
+                           "%s,%s)", (self.name, self.version + 1, t, author,
+                           remote_addr, self.text, comment, self.readonly))
+            self.version += 1
+        elif self.readonly != self.old_readonly:
+            cursor = db.cursor()
+            cursor.execute("UPDATE wiki SET readonly=%s WHERE name=%s",
+                           (self.readonly, self.name))
+        else:
+            raise TracError('Page not modified')
+
+        if handle_ta:
+            db.commit()
+
+        for listener in WikiSystem(self.env).change_listeners:
+            if self.version == 1:
+                listener.wiki_page_added(self)
+            else:
+                listener.wiki_page_changed(self, self.version, t, comment,
+                                           author, remote_addr)
+
+        self.old_readonly = self.readonly
+        self.old_text = self.text
+
+    def get_history(self, db=None):
+        if not db:
+            db = self.env.get_db_cnx()
+        cursor = db.cursor()
+        cursor.execute("SELECT version,time,author,comment,ipnr FROM wiki "
+                       "WHERE name=%s AND version<=%s "
+                       "ORDER BY version DESC", (self.name, self.version))
+        for version,time,author,comment,ipnr in cursor:
+            yield version,time,author,comment,ipnr
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/wiki/tests/__init__.py
@@ -0,0 +1,15 @@
+import unittest
+
+from trac.wiki.tests import formatter, macros, model, wikisyntax
+
+def suite():
+
+    suite = unittest.TestSuite()
+    suite.addTest(formatter.suite())
+    suite.addTest(macros.suite())
+    suite.addTest(model.suite())
+    suite.addTest(wikisyntax.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/wiki/tests/formatter.py
@@ -0,0 +1,159 @@
+import os
+import inspect
+import StringIO
+import unittest
+
+from trac.core import *
+from trac.wiki.api import IWikiSyntaxProvider
+from trac.wiki.formatter import Formatter, OneLinerFormatter
+from trac.wiki.macros import WikiMacroBase
+from trac.test import Mock, EnvironmentStub
+from trac.util.text import to_unicode
+from trac.util.markup import html
+
+# We need to supply our own macro because the real macros
+# can not be loaded using our 'fake' environment.
+
+class HelloWorldMacro(WikiMacroBase):
+    """A dummy macro used by the unit test."""
+
+    def render_macro(self, req, name, content):
+        return 'Hello World, args = ' + content
+
+class DivHelloWorldMacro(WikiMacroBase):
+    """A dummy macro returning a div block, used by the unit test."""
+
+    def render_macro(self, req, name, content):
+        return '<div>Hello World, args = %s</div>' % content
+
+class DivCodeMacro(WikiMacroBase):
+    """A dummy macro returning a div block, used by the unit test."""
+
+    def render_macro(self, req, name, content):
+        return '<div class="code">Hello World, args = %s</div>' % content
+
+class DivCodeElementMacro(WikiMacroBase):
+    """A dummy macro returning a div block, used by the unit test."""
+
+    def render_macro(self, req, name, content):
+        return html.DIV('Hello World, args = ', content, class_="code")
+
+class SampleResolver(Component):
+    """A dummy macro returning a div block, used by the unit test."""
+
+    implements(IWikiSyntaxProvider)
+
+    def get_wiki_syntax(self):
+        return []
+
+    def get_link_resolvers(self):
+        yield ('link', self._format_link)
+
+    def _format_link(self, formatter, ns, target, label):
+        kind, module = 'text', 'stuff'
+        try:
+            kind = int(target) % 2 and 'odd' or 'even'
+            module = 'thing'
+        except ValueError:
+            pass
+        return html.A(label, class_='%s resolver' % kind,
+                      href=formatter.href(module, target))
+
+
+class WikiTestCase(unittest.TestCase):
+
+    def __init__(self, input, correct, file, line):
+        unittest.TestCase.__init__(self, 'test')
+        self.title, self.input = input.split('\n', 1)
+        if self.title:
+            self.title = self.title.strip()
+        self.correct = correct
+        self.file = file
+        self.line = line
+
+        self.env = EnvironmentStub()
+        # -- macros support
+        self.env.path = ''
+        # -- intertrac support
+        self.env.config.set('intertrac', 'trac.title', "Trac's Trac")
+        self.env.config.set('intertrac', 'trac.url',
+                            "http://projects.edgewall.com/trac")
+        self.env.config.set('intertrac', 't', 'trac')
+
+        from trac.web.href import Href
+        self.req = Mock(href = Href('/'),
+                        abs_href = Href('http://www.example.com/'))
+        # TODO: remove the following lines in order to discover
+        #       all the places were we should use the req.href
+        #       instead of env.href (will be solved by the Wikifier patch)
+        self.env.href = self.req.href
+        self.env.abs_href = self.req.abs_href
+
+    def test(self):
+        """Testing WikiFormatter"""
+        out = StringIO.StringIO()
+        formatter = self.formatter()
+        formatter.format(self.input, out)
+        v = out.getvalue().replace('\r','')
+        try:
+            self.assertEquals(self.correct, v)
+        except AssertionError, e:
+            msg = to_unicode(e)
+            import re
+            match = re.match(r"u?'(.*)' != u?'(.*)'", msg)
+            if match:
+                sep = '-' * 15
+                msg = '\n%s expected:\n%s\n%s actual:\n%s\n%s\n' \
+                      % (sep, match.group(1), sep, match.group(2), sep)
+# Tip: sometimes, 'expected' and 'actual' differ only by whitespace,
+#      then replace the above line by those two:
+#                      % (sep, match.group(1).replace(' ', '.'),
+#                         sep, match.group(2).replace(' ', '.'), sep)
+                msg = msg.replace(r'\n', '\n')
+            raise AssertionError( # See below for details
+                '%s\n\n%s:%s: "%s" (%s flavor)' \
+                % (msg, self.file, self.line, self.title, formatter.flavor))
+
+    def formatter(self):
+        return Formatter(self.env, self.req)
+
+    def shortDescription(self):
+        return 'Test ' + self.title
+
+
+class OneLinerTestCase(WikiTestCase):
+    def formatter(self):
+        return OneLinerFormatter(self.env) # TODO: self.req
+
+
+def suite(data=None, setup=None, file=__file__):
+    suite = unittest.TestSuite()
+    if not data:
+        file = os.path.join(os.path.split(file)[0], 'wiki-tests.txt')
+        data = open(file, 'r').read().decode('utf-8')
+    tests = data.split('=' * 30)
+    next_line = 1
+    line = 0
+    for test in tests:
+        if line != next_line:
+            line = next_line
+        if not test or test == '\n':
+            continue
+        next_line += len(test.split('\n')) - 1
+        blocks = test.split('-' * 30 + '\n')
+        if len(blocks) != 3:
+            continue
+        input, page, oneliner = blocks
+        tc = WikiTestCase(input, page, file, line)
+        if setup:
+            setup(tc)
+        suite.addTest(tc)
+        if oneliner:
+            tc = OneLinerTestCase(input, oneliner[:-1], file, line)
+            if setup:
+                setup(tc)
+            suite.addTest(tc)
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/wiki/tests/macros.py
@@ -0,0 +1,45 @@
+import unittest
+
+import trac.wiki.macros
+from trac.wiki.tests import formatter
+
+IMAGE_MACRO_TEST_CASES=u"""
+============================== source: Image, no other arguments
+[[Image(source:test.png)]]
+------------------------------
+<p>
+<a style="padding:0; border:none" href="/browser/test.png"><img src="/browser/test.png?format=raw" alt="source:test.png" title="source:test.png" /></a>
+</p>
+------------------------------
+[[Image(...)]]
+============================== source: Image, nolink
+[[Image(source:test.png, nolink)]]
+------------------------------
+<p>
+<img src="/browser/test.png?format=raw" alt="source:test.png" title="source:test.png" />
+</p>
+------------------------------
+============================== source: Image, normal args
+[[Image(source:test.png, align=left, title=Test)]]
+------------------------------
+<p>
+<a style="padding:0; border:none" href="/browser/test.png"><img src="/browser/test.png?format=raw" alt="source:test.png" style="float:left" title="Test" /></a>
+</p>
+------------------------------
+============================== source: Image, size arg
+[[Image(source:test.png, 30%)]]
+------------------------------
+<p>
+<a style="padding:0; border:none" href="/browser/test.png"><img width="30%" alt="source:test.png" src="/browser/test.png?format=raw" title="source:test.png" /></a>
+</p>
+------------------------------
+"""
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(formatter.suite(IMAGE_MACRO_TEST_CASES, file=__file__))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/wiki/tests/model.py
@@ -0,0 +1,164 @@
+import unittest
+
+from trac.core import *
+from trac.test import EnvironmentStub
+from trac.wiki import WikiPage, IWikiChangeListener
+
+
+class TestWikiChangeListener(Component):
+    implements(IWikiChangeListener)
+    def __init__(self):
+        self.added = []
+        self.changed = []
+        self.deleted = []
+        self.deleted_version = []
+
+    def wiki_page_added(self, page):
+        self.added.append(page)
+
+    def wiki_page_changed(self, page, version, t, comment, author, ipnr):
+        self.changed.append((page, version, t, comment, author, ipnr))
+
+    def wiki_page_deleted(self, page):
+        self.deleted.append(page)
+
+    def wiki_page_version_deleted(self, page):
+        self.deleted_version.append(page)
+
+
+class WikiPageTestCase(unittest.TestCase):
+
+    def setUp(self):
+        self.env = EnvironmentStub()
+        self.db = self.env.get_db_cnx()
+
+    def test_new_page(self):
+        page = WikiPage(self.env)
+        self.assertEqual(False, page.exists)
+        self.assertEqual(None, page.name)
+        self.assertEqual(0, page.version)
+        self.assertEqual('', page.text)
+        self.assertEqual(0, page.readonly)
+
+    def test_existing_page(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO wiki VALUES(%s,%s,%s,%s,%s,%s,%s,%s)",
+                       ('TestPage', 0, 42, 'joe', '::1', 'Bla bla', 'Testing',
+                        0))
+
+        page = WikiPage(self.env, 'TestPage')
+        self.assertEqual('TestPage', page.name)
+        self.assertEqual(0, page.version)
+        self.assertEqual('Bla bla', page.text)
+        self.assertEqual(False, page.readonly)
+        history = list(page.get_history())
+        self.assertEqual(1, len(history))
+        self.assertEqual((0, 42, 'joe', 'Testing', '::1'), history[0])
+
+    def test_create_page(self):
+        page = WikiPage(self.env)
+        page.name = 'TestPage'
+        page.text = 'Bla bla'
+        page.save('joe', 'Testing', '::1', 42)
+
+        cursor = self.db.cursor()
+        cursor.execute("SELECT version,time,author,ipnr,text,comment,"
+                       "readonly FROM wiki WHERE name=%s", ('TestPage',))
+        self.assertEqual((1, 42, 'joe', '::1', 'Bla bla', 'Testing', 0),
+                         cursor.fetchone())
+
+        listener = TestWikiChangeListener(self.env)
+        self.assertEqual(page, listener.added[0])
+
+    def test_update_page(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO wiki VALUES(%s,%s,%s,%s,%s,%s,%s,%s)",
+                       ('TestPage', 1, 42, 'joe', '::1', 'Bla bla', 'Testing',
+                        0))
+
+        page = WikiPage(self.env, 'TestPage')
+        page.text = 'Bla'
+        page.save('kate', 'Changing', '192.168.0.101', 43)
+
+        cursor.execute("SELECT version,time,author,ipnr,text,comment,"
+                       "readonly FROM wiki WHERE name=%s", ('TestPage',))
+        self.assertEqual((1, 42, 'joe', '::1', 'Bla bla', 'Testing', 0),
+                         cursor.fetchone())
+        self.assertEqual((2, 43, 'kate', '192.168.0.101', 'Bla',
+                          'Changing', 0), cursor.fetchone())
+
+        listener = TestWikiChangeListener(self.env)
+        self.assertEqual((page, 2, 43, 'Changing', 'kate', '192.168.0.101'),
+                         listener.changed[0])
+
+        page = WikiPage(self.env, 'TestPage')
+        history = list(page.get_history())
+        self.assertEqual(2, len(history))
+        self.assertEqual((2, 43, 'kate', 'Changing', '192.168.0.101'),
+                         history[0])
+        self.assertEqual((1, 42, 'joe', 'Testing', '::1'), history[1])
+
+    def test_delete_page(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO wiki VALUES(%s,%s,%s,%s,%s,%s,%s,%s)",
+                       ('TestPage', 1, 42, 'joe', '::1', 'Bla bla', 'Testing',
+                        0))
+
+        page = WikiPage(self.env, 'TestPage')
+        page.delete()
+
+        self.assertEqual(False, page.exists)
+
+        cursor.execute("SELECT version,time,author,ipnr,text,comment,"
+                       "readonly FROM wiki WHERE name=%s", ('TestPage',))
+        self.assertEqual(None, cursor.fetchone())
+
+        listener = TestWikiChangeListener(self.env)
+        self.assertEqual(page, listener.deleted[0])
+
+    def test_delete_page_version(self):
+        cursor = self.db.cursor()
+        cursor.executemany("INSERT INTO wiki VALUES(%s,%s,%s,%s,%s,%s,%s,%s)",
+                           [('TestPage', 1, 42, 'joe', '::1', 'Bla bla',
+                            'Testing', 0),
+                            ('TestPage', 2, 43, 'kate', '192.168.0.101', 'Bla',
+                            'Changing', 0)])
+
+        page = WikiPage(self.env, 'TestPage')
+        page.delete(version=2)
+
+        self.assertEqual(True, page.exists)
+
+        cursor.execute("SELECT version,time,author,ipnr,text,comment,"
+                       "readonly FROM wiki WHERE name=%s", ('TestPage',))
+        self.assertEqual((1, 42, 'joe', '::1', 'Bla bla', 'Testing', 0),
+                         cursor.fetchone())
+        self.assertEqual(None, cursor.fetchone())
+
+        listener = TestWikiChangeListener(self.env)
+        self.assertEqual(page, listener.deleted_version[0])
+
+    def test_delete_page_last_version(self):
+        cursor = self.db.cursor()
+        cursor.execute("INSERT INTO wiki VALUES(%s,%s,%s,%s,%s,%s,%s,%s)",
+                       ('TestPage', 1, 42, 'joe', '::1', 'Bla bla', 'Testing',
+                        0))
+
+        page = WikiPage(self.env, 'TestPage')
+        page.delete(version=1)
+
+        self.assertEqual(False, page.exists)
+
+        cursor.execute("SELECT version,time,author,ipnr,text,comment,"
+                       "readonly FROM wiki WHERE name=%s", ('TestPage',))
+        self.assertEqual(None, cursor.fetchone())
+
+        listener = TestWikiChangeListener(self.env)
+        self.assertEqual(page, listener.deleted[0])
+
+
+def suite():
+    return unittest.makeSuite(WikiPageTestCase, 'test')
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/wiki/tests/wiki-tests.txt
@@ -0,0 +1,1159 @@
+============================================================
+
+        Font styles
+
+============================== Bold + italic markup
+This should be '''''bold and italic'''''
+------------------------------
+<p>
+This should be <strong><i>bold and italic</i></strong>
+</p>
+------------------------------
+============================== Consecutive bold + italic markup
+'''''one''''', '''''two''''', '''''three''''', '''''four'''''
+------------------------------
+<p>
+<strong><i>one</i></strong>, <strong><i>two</i></strong>, <strong><i>three</i></strong>, <strong><i>four</i></strong>
+</p>
+------------------------------
+============================== Underline + overstrike markup
+__~~underlineoversrike~~__
+------------------------------
+<p>
+<span class="underline"><del>underlineoversrike</del></span>
+</p>
+------------------------------
+============================== Problematic markup: overlapping tags
+__~~overlapping__tags~~
+------------------------------
+<p>
+<span class="underline"><del>overlapping</del></span><del>tags</del>
+</p>
+------------------------------
+============================== Problematic markup: out of order close tags
+__~~outoforderclosetags__~~
+------------------------------
+<p>
+<span class="underline"><del>outoforderclosetags</del></span><del></del>
+</p>
+------------------------------
+============================== Problematic markup: bold + missing close italic
+'''''bolditalic''' # Open italic should be closed before paragraph end
+------------------------------
+<p>
+<strong><i>bolditalic</i></strong><i> # Open italic should be closed before paragraph end
+</i></p>
+------------------------------
+============================== Italic immediately followed by bold markup
+''italic'''''bold'''
+------------------------------
+<p>
+<i>italic</i><strong>bold</strong>
+</p>
+------------------------------
+============================== Bold immediately followed by italic markup
+'''bold'''''italic''
+------------------------------
+<p>
+<strong>bold</strong><i>italic</i>
+</p>
+------------------------------
+============================== Multiline bold italic markup
+'''''bold
+italic
+multiline'''''
+------------------------------
+<p>
+<strong><i>bold
+italic
+multiline</i></strong>
+</p>
+------------------------------
+============================== Problematic multiline bold italic markup
+'''''bold
+italic
+multiline
+without endtags
+------------------------------
+<p>
+<strong><i>bold
+italic
+multiline
+without endtags
+</i></strong></p>
+------------------------------
+============================== Super and subscript markup
+^superscript^, ,,subscript,,, normal.
+------------------------------
+<p>
+<sup>superscript</sup>, <sub>subscript</sub>, normal.
+</p>
+------------------------------
+<sup>superscript</sup>, <sub>subscript</sub>, normal.
+============================== Escaping markup
+!__foo!__
+!~~bar!~~
+!,,boo!,,
+!^baz!^
+------------------------------
+<p>
+__foo__
+~~bar~~
+,,boo,,
+^baz^
+</p>
+------------------------------
+============================== Complex mixed verbatim markup
+{{{verbatim}}}
+{{{
+{{{in `block`
+}}}
+`{{{this is verbatim}}}` and {{{`that` should also `be` verbatim}}}
+------------------------------
+<p>
+<tt>verbatim</tt>
+</p>
+<pre class="wiki">{{{in `block`
+</pre><p>
+<tt>{{{this is verbatim}}}</tt> and <tt>`that` should also `be` verbatim</tt>
+</p>
+------------------------------
+<tt>verbatim</tt>
+ [&hellip;]
+<tt>{{{this is verbatim}}}</tt> and <tt>`that` should also `be` verbatim</tt>
+============================================================
+
+        Link Resolvers
+
+============================== Link resolvers, short form
+link:1
+
+Thing [link:1] Thing[link:2]
+------------------------------
+<p>
+<a class="odd resolver" href="/thing/1">link:1</a>
+</p>
+<p>
+Thing <a class="odd resolver" href="/thing/1">1</a> Thing<a class="even resolver" href="/thing/2">2</a>
+</p>
+------------------------------
+============================== Escaping links resolvers, short form
+!link:1
+Thing ![link:1 number 1], CS![link:1], ![link:bar]
+------------------------------
+<p>
+link:1
+Thing [link:1 number 1], CS[link:1], [link:bar]
+</p>
+------------------------------
+============================== Link resolvers, long form with label
+[link:1 thing one], [http://www.edgewall.com/ edgewall]
+------------------------------
+<p>
+<a class="odd resolver" href="/thing/1">thing one</a>, <a class="ext-link" href="http://www.edgewall.com/"><span class="icon">edgewall</span></a>
+</p>
+------------------------------
+============================== Link resolver SHREF_TARGET_LAST
+Add-on to link:123:
+Some change.
+link:1
+This ticket is the first one
+link:123>
+link:123&
+------------------------------
+<p>
+Add-on to <a class="odd resolver" href="/thing/123">link:123</a>:
+Some change.
+<a class="odd resolver" href="/thing/1">link:1</a>
+This ticket is the first one
+<a class="odd resolver" href="/thing/123">link:123</a>&gt;
+<a class="odd resolver" href="/thing/123">link:123</a>&amp;
+</p>
+------------------------------
+Add-on to <a class="odd resolver" href="/thing/123">link:123</a>:
+Some change.
+<a class="odd resolver" href="/thing/1">link:1</a>
+This ticket is the first one
+<a class="odd resolver" href="/thing/123">link:123</a>&gt;
+<a class="odd resolver" href="/thing/123">link:123</a>&amp;
+============================== Link resolver SHREF_TARGET_FIRST
+<bug>http://localhost/bugzilla/show_bug.cgi?id=1284</bug> 804
+------------------------------
+<p>
+&lt;bug&gt;<a class="ext-link" href="http://localhost/bugzilla/show_bug.cgi?id=1284"><span class="icon">http://localhost/bugzilla/show_bug.cgi?id=1284</span></a>&lt;/bug&gt; 804
+</p>
+------------------------------
+============================== Link resolver SHREF_TARGET_MIDDLE
+||http://example.com/img.png||text||
+------------------------------
+<table class="wiki">
+<tr><td><a class="ext-link" href="http://example.com/img.png"><span class="icon">http://example.com/img.png</span></a></td><td>text
+</td></tr></table>
+------------------------------
+||<a class="ext-link" href="http://example.com/img.png"><span class="icon">http://example.com/img.png</span></a>||text||
+============================== Link resolver, long form with quoting
+[link:WikiStart Foo] [http://www.edgewall.com/ Edgewall]
+
+link:"Foo Bar" link:"Foo Bar#baz"
+
+[link:"Foo Bar" Foo Bar] [link:"Foo Bar#baz" Foo Bar]
+
+[link:Argv "*argv[] versus **argv"]
+
+[link:test "test.txt", line 123]
+
+[link:pl/de %de]
+------------------------------
+<p>
+<a class="text resolver" href="/stuff/WikiStart">Foo</a> <a class="ext-link" href="http://www.edgewall.com/"><span class="icon">Edgewall</span></a>
+</p>
+<p>
+<a class="text resolver" href="/stuff/Foo%20Bar">link:"Foo Bar"</a> <a class="text resolver" href="/stuff/Foo%20Bar%23baz">link:"Foo Bar#baz"</a>
+</p>
+<p>
+<a class="text resolver" href="/stuff/Foo%20Bar">Foo Bar</a> <a class="text resolver" href="/stuff/Foo%20Bar%23baz">Foo Bar</a>
+</p>
+<p>
+<a class="text resolver" href="/stuff/Argv">*argv[] versus **argv</a>
+</p>
+<p>
+<a class="text resolver" href="/stuff/test">"test.txt", line 123</a>
+</p>
+<p>
+<a class="text resolver" href="/stuff/pl/de">%de</a>
+</p>
+------------------------------
+============================== Link resolver in markup
+'''link:1''', ''link:1'', ~~link:1~~, __link:1__
+------------------------------
+<p>
+<strong><a class="odd resolver" href="/thing/1">link:1</a></strong>, <i><a class="odd resolver" href="/thing/1">link:1</a></i>, <del><a class="odd resolver" href="/thing/1">link:1</a></del>, <span class="underline"><a class="odd resolver" href="/thing/1">link:1</a></span>
+</p>
+------------------------------
+============================== Link resolver, quoting of target
+link:1
+link:12
+link:123
+link:'1'
+link:'12'
+link:'123'
+link:"1"
+link:"12"
+link:"123"
+------------------------------
+<p>
+<a class="odd resolver" href="/thing/1">link:1</a>
+<a class="even resolver" href="/thing/12">link:12</a>
+<a class="odd resolver" href="/thing/123">link:123</a>
+<a class="odd resolver" href="/thing/1">link:'1'</a>
+<a class="even resolver" href="/thing/12">link:'12'</a>
+<a class="odd resolver" href="/thing/123">link:'123'</a>
+<a class="odd resolver" href="/thing/1">link:"1"</a>
+<a class="even resolver" href="/thing/12">link:"12"</a>
+<a class="odd resolver" href="/thing/123">link:"123"</a>
+</p>
+------------------------------
+============================================================
+
+        Other Links
+
+============================== Relative links
+Relative links are supported:
+[../parent See above]
+[/docs See documentation]
+[/images/logo.png Our logo]
+[/]
+------------------------------
+<p>
+Relative links are supported:
+<a href="../parent">See above</a>
+<a href="/docs">See documentation</a>
+<a href="/images/logo.png">Our logo</a>
+<a href="/">/</a>
+</p>
+------------------------------
+==============================  Image links are now regular external links
+http://example.com/img.png?foo=bar
+------------------------------
+<p>
+<a class="ext-link" href="http://example.com/img.png?foo=bar"><span class="icon">http://example.com/img.png?foo=bar</span></a>
+</p>
+------------------------------
+<a class="ext-link" href="http://example.com/img.png?foo=bar"><span class="icon">http://example.com/img.png?foo=bar</span></a>
+============================== Arbitrary protocol Link
+''RFCs von ftp://ftp.rfc-editor.org/in-notes/rfcXXXX.txt''
+------------------------------
+<p>
+<i>RFCs von <a class="ext-link" href="ftp://ftp.rfc-editor.org/in-notes/rfcXXXX.txt"><span class="icon">ftp://ftp.rfc-editor.org/in-notes/rfcXXXX.txt</span></a></i>
+</p>
+------------------------------
+============================== Another arbitrary protocol Link
+svn+ssh://secureserver.org
+[svn+ssh://secureserver.org SVN link]
+rfc-2396.compatible://link
+[rfc-2396.compatible://link RFC 2396]
+------------------------------
+<p>
+<a class="ext-link" href="svn+ssh://secureserver.org"><span class="icon">svn+ssh://secureserver.org</span></a>
+<a class="ext-link" href="svn+ssh://secureserver.org"><span class="icon">SVN link</span></a>
+<a class="ext-link" href="rfc-2396.compatible://link"><span class="icon">rfc-2396.compatible://link</span></a>
+<a class="ext-link" href="rfc-2396.compatible://link"><span class="icon">RFC 2396</span></a>
+</p>
+------------------------------
+============================== Link resolver counter examples
+Test:[[BR]] There should be a line break
+
+Other test:'''bold text''' should be bold
+------------------------------
+<p>
+Test:<br /> There should be a line break
+</p>
+<p>
+Other test:<strong>bold text</strong> should be bold
+</p>
+------------------------------
+Test:  There should be a line break
+
+Other test:<strong>bold text</strong> should be bold
+============================== Link resolver counter example
+'''Note:'''
+------------------------------
+<p>
+<strong>Note:</strong>
+</p>
+------------------------------
+<strong>Note:</strong>
+==============================
+============================================================
+
+        Processor blocks
+
+============================== Code Block 1
+{{{
+Preformatted text.
+}}}
+Paragraph
+------------------------------
+<pre class="wiki">Preformatted text.
+</pre><p>
+Paragraph
+</p>
+------------------------------
+ [&hellip;]
+Paragraph
+============================== Code Block 2
+{{{
+<b>Preformatted text</b>.
+}}}
+Paragraph
+------------------------------
+<pre class="wiki">&lt;b&gt;Preformatted text&lt;/b&gt;.
+</pre><p>
+Paragraph
+</p>
+------------------------------
+ [&hellip;]
+Paragraph
+============================== Embedded code blocks
+{{{
+Outer block.
+{{{
+Inner block.
+}}}
+}}}
+Paragraph
+------------------------------
+<pre class="wiki">Outer block.
+{{{
+Inner block.
+}}}
+</pre><p>
+Paragraph
+</p>
+------------------------------
+ [&hellip;]
+Paragraph
+============================== Consecutive code blocks
+Block 
+{{{
+number one
+}}}
+and block
+{{{
+number two
+}}}
+.
+------------------------------
+<p>
+Block 
+</p>
+<pre class="wiki">number one
+</pre><p>
+and block
+</p>
+<pre class="wiki">number two
+</pre><p>
+.
+</p>
+------------------------------
+Block 
+ [&hellip;]
+and block
+ [&hellip;]
+.
+============================== Unfinished code blocks
+Block 
+{{{
+number one
+
+and block
+{{{
+number two
+ }}
+------------------------------
+<p>
+Block 
+</p>
+<pre class="wiki">number one
+
+and block
+{{{
+number two
+ }}
+}}}
+</pre>------------------------------
+Block [&hellip;]
+============================== Wiki processor
+{{{
+#!default
+Preformatted text.
+}}}
+Paragraph
+------------------------------
+<pre class="wiki">Preformatted text.
+</pre><p>
+Paragraph
+</p>
+------------------------------
+ [&hellip;]
+Paragraph
+============================== Wiki processor counter example
+{{{
+#!/bin/sh
+echo "foo"
+}}}
+Paragraph
+------------------------------
+<pre class="wiki">#!/bin/sh
+echo "foo"
+</pre><p>
+Paragraph
+</p>
+------------------------------
+ [&hellip;]
+Paragraph
+============================== HTML wiki processor
+{{{
+#!html
+<p>Hello World</p>
+}}}
+------------------------------
+<p>Hello World</p>
+------------------------------
+ [&hellip;]
+============================== HTML wiki processor, XSS check 1
+{{{
+#!html
+<script>alert("");</script>
+}}}
+------------------------------
+
+------------------------------
+ [&hellip;]
+============================== HTML wiki processor, XSS check 2
+{{{
+#!html
+<div onclick="alert('')">Click me</div>
+}}}
+------------------------------
+<div>Click me</div>
+------------------------------
+ [&hellip;]
+============================================================
+
+        Wiki Macros
+
+============================== Macro with arguments (leading)
+[[HelloWorld(hej hopp)]]
+------------------------------
+<p>
+Hello World, args = hej hopp
+</p>
+------------------------------
+[[HelloWorld(...)]]
+============================== Macro with arguments (in flow)
+Hello, [[HelloWorld(hej hopp)]]
+------------------------------
+<p>
+Hello, Hello World, args = hej hopp
+</p>
+------------------------------
+Hello, [[HelloWorld(...)]]
+============================== Bad macro call
+[[HelloWorld(hej hopp) ]] # This shouldnt executed as macro since it contain whitespace between ) and ]
+------------------------------
+<p>
+<a class="missing wiki" href="/wiki/%5BHelloWorld%28hej" rel="nofollow">hopp) ?</a>] # This shouldnt executed as macro since it contain whitespace between ) and ]
+</p>
+------------------------------
+<a class="missing wiki" href="/wiki/%5BHelloWorld%28hej" rel="nofollow">hopp) ?</a>] # This shouldnt executed as macro since it contain whitespace between ) and ]
+============================== Another bad macro call
+[[HelloWorld(hej hopp))]] # Extra right brace and still executed
+------------------------------
+<p>
+Hello World, args = hej hopp) # Extra right brace and still executed
+</p>
+------------------------------
+[[HelloWorld(...)]] # Extra right brace and still executed
+============================== Two consecutive macros on a line
+[[HelloWorld(hej hopp)]] [[HelloWorld(hej hopp2)]] # Test non greedy match
+------------------------------
+<p>
+Hello World, args = hej hopp Hello World, args = hej hopp2 # Test non greedy match
+</p>
+------------------------------
+[[HelloWorld(...)]] [[HelloWorld(...)]] # Test non greedy match
+============================== Macro returning a <div>
+[[DivHelloWorld(hej hopp)]]
+------------------------------
+<p>
+</p><div>Hello World, args = hej hopp</div><p>
+</p>
+------------------------------
+[[DivHelloWorld(...)]]
+============================== Macro returning a <div class="...code...">
+[[DivCode(hej hopp)]]
+------------------------------
+<p>
+<span class="code-block">Hello World, args = hej hopp</span>
+</p>
+------------------------------
+[[DivCode(...)]]
+============================== Macro returning an html.DIV(class="...code...">)
+[[DivCodeElement(hej hopp)]]
+------------------------------
+<p>
+<span class="code-block">Hello World, args = hej hopp</span>
+</p>
+------------------------------
+[[DivCodeElement(...)]]
+============================== Inlined HTML wiki processor
+Inline [[html(<B> Test </B>)]] text
+------------------------------
+<p>
+Inline <b> Test </b> text
+</p>
+------------------------------
+Inline [[html(...)]] text
+============================== BR macro
+Line break [[BR]] another line[[br]]last line
+------------------------------
+<p>
+Line break <br /> another line<br />last line
+</p>
+------------------------------
+Line break   another line last line
+============================== Comment wiki processor
+Test comment blocks
+{{{
+#!comment
+This is simply removed from the output
+}}}
+------------------------------
+<p>
+Test comment blocks
+</p>
+------------------------------
+Test comment blocks
+============================== Comment wiki processor called as a macro
+Inline [[comment(This should not be seen)]] comment
+------------------------------
+<p>
+Inline  comment
+</p>
+------------------------------
+Inline  comment
+============================================================
+
+           Headings
+
+============================== I18N heading
+= ça marche! =
+------------------------------
+<h1 id="çamarche">ça marche!</h1>
+------------------------------
+= ça marche! =
+============================== Quoted heading
+= "Test" =
+------------------------------
+<h1 id="Test">"Test"</h1>
+------------------------------
+= "Test" =
+============================== Heading with < and >
+= Foo <Bar> Baz =
+------------------------------
+<h1 id="FooBarBaz">Foo &lt;Bar&gt; Baz</h1>
+------------------------------
+= Foo &lt;Bar&gt; Baz =
+============================== Heading with .
+= Version 0.10 =
+------------------------------
+<h1 id="Version0.10">Version 0.10</h1>
+------------------------------
+= Version 0.10 =
+============================== Normal heading
+== Heading with trailing white-space == 
+------------------------------
+<h2 id="Headingwithtrailingwhite-space">Heading with trailing white-space</h2>
+------------------------------
+== Heading with trailing white-space ==
+============================== Formatted heading
+== ''Formatted'' ~~Heading~~ ==
+------------------------------
+<h2 id="FormattedHeading"><i>Formatted</i> <del>Heading</del></h2>
+------------------------------
+== ''Formatted'' ~~Heading~~ ==
+============================== Heading with link
+== [wiki:SandBox Linked Heading] ==
+------------------------------
+<h2 id="LinkedHeading"><a class="missing wiki" href="/wiki/SandBox" rel="nofollow">Linked Heading?</a></h2>
+------------------------------
+== [wiki:SandBox Linked Heading] ==
+============================== Normal heading, fixed id
+== Heading with fixed id == #heading-fixed-id
+------------------------------
+<h2 id="heading-fixed-id">Heading with fixed id</h2>
+------------------------------
+== Heading with fixed id == #heading-fixed-id
+============================== Normal heading, auto-corrected id
+== 10 tips ==
+------------------------------
+<h2 id="a10tips">10 tips</h2>
+------------------------------
+== 10 tips ==
+============================================================
+
+             Lists
+
+============================== Bulleted lists
+Paragraph
+ * foo bar
+   boo baz
+   * Subitem
+     Subitem line 2
+ * item 2
+   item 2 line 2
+Paragraph
+------------------------------
+<p>
+Paragraph
+</p>
+<ul><li>foo bar
+boo baz
+<ul><li>Subitem
+Subitem line 2
+</li></ul></li><li>item 2
+item 2 line 2
+</li></ul><p>
+Paragraph
+</p>
+------------------------------
+Paragraph
+ * foo bar
+   boo baz
+   * Subitem
+     Subitem line 2
+ * item 2
+   item 2 line 2
+Paragraph
+============================== Changelog sample
+2003-09-18 23:26  Joe Bar <joeb@gloogle.gom>
+
+	* src/code.py: Fix problem with obsolete use of
+	backslash in symbols.
+        * src/test.py: Added unit tests.
+          - test + symbol
+          - test - symbol
+Paragraph
+------------------------------
+<p>
+2003-09-18 23:26  Joe Bar &lt;joeb@gloogle.gom&gt;
+</p>
+<ul><li>src/code.py: Fix problem with obsolete use of
+backslash in symbols.
+</li><li>src/test.py: Added unit tests.
+<ul><li>test + symbol
+</li><li>test - symbol
+</li></ul></li></ul><p>
+Paragraph
+</p>
+------------------------------
+============================== Complex bulleted list
+    * foo bar
+      boo baz
+           * Subitem 1
+             - nested item 1
+             - nested item 2
+             nested item 2 continued
+           Subitem 1 continued
+        * Subitem 2
+         Subitem 2 continued
+        * Subitem 3
+          continued
+    * item 2
+     item 2 line 2
+Paragraph
+------------------------------
+<ul><li>foo bar
+boo baz
+<ul><li>Subitem 1
+<ul><li>nested item 1
+</li><li>nested item 2
+nested item 2 continued
+</li></ul></li><li>Subitem 1 continued
+</li></ul></li><li>Subitem 2
+Subitem 2 continued
+</li><li>Subitem 3
+continued
+</li></ul><ul><li>item 2
+item 2 line 2
+</li></ul><p>
+Paragraph
+</p>
+------------------------------
+============================== Numbered lists
+ 1. item 1
+   a. item 1.a
+   a. item 1.b
+
+Some paragraph
+
+ 2. continue with item 2
+    i. roman 1
+    ii. roman 2
+Paragraph
+------------------------------
+<ol><li>item 1
+<ol class="loweralpha"><li>item 1.a
+</li><li>item 1.b
+</li></ol></li></ol><p>
+Some paragraph
+</p>
+<ol start="2"><li>continue with item 2
+<ol class="lowerroman"><li>roman 1
+</li><li>roman 2
+</li></ol></li></ol><p>
+Paragraph
+</p>
+------------------------------
+1. item 1
+   a. item 1.a
+   a. item 1.b
+
+Some paragraph
+
+ 2. continue with item 2
+    i. roman 1
+    ii. roman 2
+Paragraph
+============================== Numbered lists multi-line items
+ 1. This is a very long line at
+    the first level, which works correctly.
+    1. But this line at the second level, which
+       is also continued on the next line, does not.
+Paragraph
+------------------------------
+<ol><li>This is a very long line at
+the first level, which works correctly.
+<ol><li>But this line at the second level, which
+is also continued on the next line, does not.
+</li></ol></li></ol><p>
+Paragraph
+</p>
+------------------------------
+1. This is a very long line at
+    the first level, which works correctly.
+    1. But this line at the second level, which
+       is also continued on the next line, does not.
+Paragraph
+============================== Numbered lists counter-examples
+ This will not start a new numbered
+ list. There's more than one character
+ before the "."
+ OTOH, the following is a roman numbered list:
+ iii. start
+ xxvii. maximal number in sequence
+Paragraph
+------------------------------
+<blockquote>
+<p>
+This will not start a new numbered
+list. There's more than one character
+before the "."
+OTOH, the following is a roman numbered list:
+</p>
+</blockquote>
+<ol class="lowerroman"><li>start
+</li><li>maximal number in sequence
+</li></ol><p>
+Paragraph
+</p>
+------------------------------
+============================== Mixed lists multi-line items
+ 1. multi-line
+    numbered list
+    i. multi-line
+      item i.
+      * sub item
+    ii. multi-line
+      item ii.
+      * sub item
+        multiline
+        a. subsub
+          multiline
+        b. subsub
+    iii. multi-line
+      item iii.
+      * sub item
+Paragraph
+------------------------------
+<ol><li>multi-line
+numbered list
+<ol class="lowerroman"><li>multi-line
+item i.
+<ul><li>sub item
+</li></ul></li><li>multi-line
+item ii.
+<ul><li>sub item
+multiline
+<ol class="loweralpha"><li>subsub
+multiline
+</li><li>subsub
+</li></ol></li></ul></li><li>multi-line
+item iii.
+<ul><li>sub item
+</li></ul></li></ol></li></ol><p>
+Paragraph
+</p>
+------------------------------
+============================== Simple definition list
+ term:: definition
+------------------------------
+<dl><dt>term</dt><dd>definition
+</dd></dl>
+------------------------------
+term:: definition
+============================== Tricky definition list
+ term:: definition:: text
+------------------------------
+<dl><dt>term</dt><dd>definition:: text
+</dd></dl>
+------------------------------
+term:: definition:: text
+============================== Verbatim term in definition list
+ `term`:: definition
+------------------------------
+<dl><dt><tt>term</tt></dt><dd>definition
+</dd></dl>
+------------------------------
+<tt>term</tt>:: definition
+============================== Another verbatim term in definition list
+ {{{term}}}:: definition
+------------------------------
+<dl><dt><tt>term</tt></dt><dd>definition
+</dd></dl>
+------------------------------
+<tt>term</tt>:: definition
+============================== Complex definition list
+ complex topic:: multiline
+                 ''formatted''
+                 definition
+------------------------------
+<dl><dt>complex topic</dt><dd>multiline
+<i>formatted</i>
+definition
+</dd></dl>
+------------------------------
+complex topic:: multiline
+                 <i>formatted</i>
+                 definition
+============================== Definition list counter example
+ term::definition
+------------------------------
+<blockquote>
+<p>
+term::definition
+</p>
+</blockquote>
+------------------------------
+term::definition
+============================== Definition list + escaped definition list
+ complex topic:: multiline
+                 `not:: a dl`
+------------------------------
+<dl><dt>complex topic</dt><dd>multiline
+<tt>not:: a dl</tt>
+</dd></dl>
+------------------------------
+complex topic:: multiline
+                 <tt>not:: a dl</tt>
+============================== Definition list + another escaped definition list
+ complex topic:: multiline
+                 {{{not:: a dl}}}
+------------------------------
+<dl><dt>complex topic</dt><dd>multiline
+<tt>not:: a dl</tt>
+</dd></dl>
+------------------------------
+complex topic:: multiline
+                 <tt>not:: a dl</tt>
+============================================================
+
+            Tables
+
+============================== Simple Table, one column
+|| a || 
+|| b ||
+------------------------------
+<table class="wiki">
+<tr><td> a 
+</td></tr><tr><td> b 
+</td></tr></table>
+------------------------------
+|| a || 
+|| b ||
+============================== Simple Table, multiple columns
+in:
+|| RPC# || parameter len || ..... parameter ..... ||
+out:
+|| RPC# || parameter len || ..... parameter ..... ||
+----
+------------------------------
+<p>
+in:
+</p>
+<table class="wiki">
+<tr><td> RPC# </td><td> parameter len </td><td> ..... parameter ..... 
+</td></tr></table>
+<p>
+out:
+</p>
+<table class="wiki">
+<tr><td> RPC# </td><td> parameter len </td><td> ..... parameter ..... 
+</td></tr></table>
+<hr />
+------------------------------
+in:
+|| RPC# || parameter len || ..... parameter ..... ||
+out:
+|| RPC# || parameter len || ..... parameter ..... ||
+----
+============================== Indented tables, multiple columns
+|| a || b ||
+
+  || a || b ||
+  || a || b ||
+
+|| a || b ||
+------------------------------
+<table class="wiki">
+<tr><td> a </td><td> b 
+</td></tr></table>
+<blockquote>
+<table class="wiki">
+<tr><td> a </td><td> b 
+</td></tr><tr><td> a </td><td> b 
+</td></tr></table>
+</blockquote>
+<table class="wiki">
+<tr><td> a </td><td> b 
+</td></tr></table>
+------------------------------
+|| a || b ||
+
+  || a || b ||
+  || a || b ||
+
+|| a || b ||
+============================================================
+
+         Mixed examples
+
+============================== Mix of headings and lists
+= Heading 1 =
+Paragraph
+ * Item 1
+   * Item 2
+Another paragraph
+------------------------------
+<h1 id="Heading1">Heading 1</h1>
+<p>
+Paragraph
+</p>
+<ul><li>Item 1
+<ul><li>Item 2
+</li></ul></li></ul><p>
+Another paragraph
+</p>
+------------------------------
+= Heading 1 =
+Paragraph
+ * Item 1
+   * Item 2
+Another paragraph
+============================== Heading, lists and table
+Paragraph
+----
+ 1. Item 1
+   2. Item 2
+||Table||cell||
+||Foo||Bar||Baz||
+http://www.edgewall.com/
+------------------------------
+<p>
+Paragraph
+</p>
+<hr />
+<ol><li>Item 1
+<ol start="2"><li>Item 2
+</li></ol></li></ol><table class="wiki">
+<tr><td>Table</td><td>cell
+</td></tr><tr><td>Foo</td><td>Bar</td><td>Baz
+</td></tr></table>
+<p>
+<a class="ext-link" href="http://www.edgewall.com/"><span class="icon">http://www.edgewall.com/</span></a>
+</p>
+------------------------------
+Paragraph
+----
+ 1. Item 1
+   2. Item 2
+||Table||cell||
+||Foo||Bar||Baz||
+<a class="ext-link" href="http://www.edgewall.com/"><span class="icon">http://www.edgewall.com/</span></a>
+============================== Lists, indents and table
+
+  * Bar
+    * Foo
+
+    || Foo || Bar ||
+
+  || Foo || Bar ||
+------------------------------
+<ul><li>Bar
+<ul><li>Foo
+</li></ul></li></ul><blockquote>
+<blockquote>
+<table class="wiki">
+<tr><td> Foo </td><td> Bar 
+</td></tr></table>
+</blockquote>
+</blockquote>
+<blockquote>
+<table class="wiki">
+<tr><td> Foo </td><td> Bar 
+</td></tr></table>
+</blockquote>
+------------------------------
+* Bar
+    * Foo
+
+    || Foo || Bar ||
+
+  || Foo || Bar ||
+============================== "Tabstops" set by lists and quotes
+        This is one level deep
+
+  * Bar
+        * Foo
+
+        Now this should be 2 levels deep as well
+
+               This is now level 3.
+
+        Continue on level 2.
+
+          - but a list always restart at level 1.
+Paragraph.
+------------------------------
+<blockquote>
+<p>
+This is one level deep
+</p>
+</blockquote>
+<ul><li>Bar
+<ul><li>Foo
+</li></ul></li></ul><blockquote>
+<blockquote>
+<p>
+Now this should be 2 levels deep as well
+</p>
+</blockquote>
+</blockquote>
+<blockquote>
+<blockquote>
+<blockquote>
+<p>
+This is now level 3.
+</p>
+</blockquote>
+</blockquote>
+</blockquote>
+<blockquote>
+<blockquote>
+<p>
+Continue on level 2.
+</p>
+</blockquote>
+</blockquote>
+<ul><li>but a list always restart at level 1.
+</li></ul><p>
+Paragraph.
+</p>
+------------------------------
+============================== Citations
+> This is the quoted text
+>> a nested quote
+A comment on the above
+>> start 2nd level
+> first level
+------------------------------
+<blockquote class="citation">
+<p>
+ This is the quoted text
+</p>
+<blockquote class="citation">
+<p>
+ a nested quote
+</p>
+</blockquote>
+</blockquote>
+<p>
+A comment on the above
+</p>
+<blockquote class="citation">
+<blockquote class="citation">
+<p>
+ start 2nd level
+</p>
+</blockquote>
+<p>
+ first level
+</p>
+</blockquote>
+------------------------------
+&gt; This is the quoted text
+&gt;&gt; a nested quote
+A comment on the above
+&gt;&gt; start 2nd level
+&gt; first level
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/wiki/tests/wikisyntax.py
@@ -0,0 +1,149 @@
+import unittest
+
+from trac.wiki.api import WikiSystem
+from trac.wiki.model import WikiPage
+from trac.wiki.tests import formatter
+
+TEST_CASES=u"""
+============================== wiki: link resolver
+wiki:TestPage
+wiki:TestPage/
+wiki:"Space 1 23"
+wiki:"C'est l'\xe9t\xe9"
+wiki:MissingPage
+wiki:12
+wiki:abc
+------------------------------
+<p>
+<a class="wiki" href="/wiki/TestPage">wiki:TestPage</a>
+<a class="wiki" href="/wiki/TestPage">wiki:TestPage/</a>
+<a class="wiki" href="/wiki/Space%201%2023">wiki:"Space 1 23"</a>
+<a class="wiki" href="/wiki/C%27est%20l%27%C3%A9t%C3%A9">wiki:"C'est l'\xe9t\xe9"</a>
+<a class="missing wiki" href="/wiki/MissingPage" rel="nofollow">wiki:MissingPage?</a>
+<a class="missing wiki" href="/wiki/12" rel="nofollow">wiki:12?</a>
+<a class="missing wiki" href="/wiki/abc" rel="nofollow">wiki:abc?</a>
+</p>
+------------------------------
+============================== WikiPageNames conformance
+CamelCase AlabamA ABc AlaBamA FooBar
+------------------------------
+<p>
+<a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a> AlabamA ABc AlaBamA <a class="missing wiki" href="/wiki/FooBar" rel="nofollow">FooBar?</a>
+</p>
+------------------------------
+============================== More WikiPageNames conformance
+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>.<a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a>: <a class="missing wiki" href="/wiki/CamelCase" rel="nofollow">CamelCase?</a>
+</p>
+------------------------------
+============================== Escaping WikiPageNames
+!CamelCase
+------------------------------
+<p>
+CamelCase
+</p>
+------------------------------
+============================== WikiPageNames endings
+foo (FooBar)
+
+foo (FooBar )
+------------------------------
+<p>
+foo (<a class="missing wiki" href="/wiki/FooBar" rel="nofollow">FooBar?</a>)
+</p>
+<p>
+foo (<a class="missing wiki" href="/wiki/FooBar" rel="nofollow">FooBar?</a> )
+</p>
+------------------------------
+============================== WikiPageNames counter examples
+A0B1, ST62T53C6, IR32V1H000
+------------------------------
+<p>
+A0B1, ST62T53C6, IR32V1H000
+</p>
+------------------------------
+============================== WikiPageNames trailing characters
+SandBox SandBox, SandBox; SandBox: SandBox. SandBox? SandBox! (SandBox) {SandBox} [SandBox]
+------------------------------
+<p>
+<a class="missing wiki" href="/wiki/SandBox" rel="nofollow">SandBox?</a> <a class="missing wiki" href="/wiki/SandBox" rel="nofollow">SandBox?</a>, <a class="missing wiki" href="/wiki/SandBox" rel="nofollow">SandBox?</a>; <a class="missing wiki" href="/wiki/SandBox" rel="nofollow">SandBox?</a>: <a class="missing wiki" href="/wiki/SandBox" rel="nofollow">SandBox?</a>. <a class="missing wiki" href="/wiki/SandBox" rel="nofollow">SandBox?</a>? <a class="missing wiki" href="/wiki/SandBox" rel="nofollow">SandBox?</a>! (<a class="missing wiki" href="/wiki/SandBox" rel="nofollow">SandBox?</a>) {<a class="missing wiki" href="/wiki/SandBox" rel="nofollow">SandBox?</a>} <a class="missing wiki" href="/wiki/SandBox" rel="nofollow">SandBox?</a>
+</p>
+------------------------------
+============================== WikiPageNames counter examples (paths)
+/absolute/path/is/NotWiki and relative/path/is/NotWiki
+/ThisIsNotWikiEither and /ThisIs/NotWikiEither but ThisIs/SubWiki
+------------------------------
+<p>
+/absolute/path/is/NotWiki and relative/path/is/NotWiki
+/ThisIsNotWikiEither and /ThisIs/NotWikiEither but <a class="missing wiki" href="/wiki/ThisIs/SubWiki" rel="nofollow">ThisIs/SubWiki?</a>
+</p>
+------------------------------
+============================== WikiPageNames counter examples (numbers)
+8FjBpOmy
+anotherWikiPageName
+------------------------------
+<p>
+8FjBpOmy
+anotherWikiPageName
+</p>
+------------------------------
+8FjBpOmy
+anotherWikiPageName
+============================== MoinMoin style forced links
+This is a ["Wiki"] page link.
+------------------------------
+<p>
+This is a <a class="missing wiki" href="/wiki/Wiki" rel="nofollow">Wiki?</a> page link.
+</p>
+------------------------------
+============================== InterTrac for wiki
+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">t:wiki:InterTrac</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/wiki/InterTrac" title="wiki:InterTrac in Trac's Trac"><span class="icon">trac:wiki:InterTrac</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/wiki/InterTrac" title="wiki:InterTrac in Trac's Trac"><span class="icon">intertrac</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/wiki/InterTrac" title="wiki:InterTrac in Trac's Trac"><span class="icon">intertrac</span></a>
+</p>
+------------------------------
+============================== Wiki InterTrac shorthands
+t:InterTrac
+trac:InterTrac
+[t:InterTrac intertrac]
+[trac:InterTrac intertrac]
+------------------------------
+<p>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=InterTrac" title="InterTrac in Trac's Trac"><span class="icon">t:InterTrac</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=InterTrac" title="InterTrac in Trac's Trac"><span class="icon">trac:InterTrac</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=InterTrac" title="InterTrac in Trac's Trac"><span class="icon">intertrac</span></a>
+<a class="ext-link" href="http://projects.edgewall.com/trac/search?q=InterTrac" title="InterTrac in Trac's Trac"><span class="icon">intertrac</span></a>
+</p>
+------------------------------
+"""
+
+def wiki_setup(tc):
+    wiki1 = WikiPage(tc.env)
+    wiki1.name = 'TestPage'
+    wiki1.text = '--'
+    wiki1.save('joe', 'normal WikiPageNames', '::1', 42)
+
+    wiki2 = WikiPage(tc.env)
+    wiki2.name = 'Space 1 23'
+    wiki2.text = '--'
+    wiki2.save('joe', 'not a WikiPageNames', '::1', 42)
+
+    wiki3 = WikiPage(tc.env)
+    wiki3.name = u"C'est l'\xe9t\xe9"
+    wiki3.text = '--'
+    wiki3.save('joe', 'unicode WikiPageNames', '::1', 42)
+
+def suite():
+    return formatter.suite(TEST_CASES, wiki_setup, __file__)
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite') 
new file mode 100644
--- /dev/null
+++ b/examples/trac/trac/wiki/web_ui.py
@@ -0,0 +1,491 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2003-2006 Edgewall Software
+# Copyright (C) 2003-2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2004-2005 Christopher Lenz <cmlenz@gmx.de>
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+#
+# Author: Jonas Borgström <jonas@edgewall.com>
+#         Christopher Lenz <cmlenz@gmx.de>
+
+import os
+import re
+import StringIO
+
+from trac.attachment import attachments_to_hdf, Attachment, AttachmentModule
+from trac.core import *
+from trac.perm import IPermissionRequestor
+from trac.Search import ISearchSource, search_to_sql, shorten_result
+from trac.Timeline import ITimelineEventProvider
+from trac.util import get_reporter_id
+from trac.util.datefmt import format_datetime, pretty_timedelta
+from trac.util.text import shorten_line
+from trac.util.markup import html, Markup
+from trac.versioncontrol.diff import get_diff_options, hdf_diff
+from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
+from trac.web import HTTPNotFound, IRequestHandler
+from trac.wiki.api import IWikiPageManipulator, WikiSystem
+from trac.wiki.model import WikiPage
+from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner
+from trac.mimeview.api import Mimeview, IContentConverter
+
+
+class InvalidWikiPage(TracError):
+    """Exception raised when a Wiki page fails validation."""
+
+
+class WikiModule(Component):
+
+    implements(INavigationContributor, IPermissionRequestor, IRequestHandler,
+               ITimelineEventProvider, ISearchSource, IContentConverter)
+
+    page_manipulators = ExtensionPoint(IWikiPageManipulator)
+
+    # IContentConverter methods
+    def get_supported_conversions(self):
+        yield ('txt', 'Plain Text', 'txt', 'text/x-trac-wiki', 'text/plain', 9)
+
+    def convert_content(self, req, mimetype, content, key):
+        return (content, 'text/plain;charset=utf-8')
+
+    # INavigationContributor methods
+
+    def get_active_navigation_item(self, req):
+        return 'wiki'
+
+    def get_navigation_items(self, req):
+        if not req.perm.has_permission('WIKI_VIEW'):
+            return
+        yield ('mainnav', 'wiki',
+               html.A('Wiki', href=req.href.wiki(), accesskey=1))
+        yield ('metanav', 'help',
+               html.A('Help/Guide', href=req.href.wiki('TracGuide'),
+                      accesskey=6))
+
+    # IPermissionRequestor methods
+
+    def get_permission_actions(self):
+        actions = ['WIKI_CREATE', 'WIKI_DELETE', 'WIKI_MODIFY', 'WIKI_VIEW']
+        return actions + [('WIKI_ADMIN', actions)]
+
+    # IRequestHandler methods
+
+    def match_request(self, req):
+        match = re.match(r'^/wiki(?:/(.*))?', req.path_info)
+        if match:
+            if match.group(1):
+                req.args['page'] = match.group(1)
+            return 1
+
+    def process_request(self, req):
+        action = req.args.get('action', 'view')
+        pagename = req.args.get('page', 'WikiStart')
+        version = req.args.get('version')
+
+        if pagename.endswith('/'):
+            req.redirect(req.href.wiki(pagename.strip('/')))
+
+        db = self.env.get_db_cnx()
+        page = WikiPage(self.env, pagename, version, db)
+
+        add_stylesheet(req, 'common/css/wiki.css')
+
+        if req.method == 'POST':
+            if action == 'edit':
+                latest_version = WikiPage(self.env, pagename, None, db).version
+                if req.args.has_key('cancel'):
+                    req.redirect(req.href.wiki(page.name))
+                elif int(version) != latest_version:
+                    action = 'collision'
+                    self._render_editor(req, db, page)
+                elif req.args.has_key('preview'):
+                    action = 'preview'
+                    self._render_editor(req, db, page, preview=True)
+                else:
+                    self._do_save(req, db, page)
+            elif action == 'delete':
+                self._do_delete(req, db, page)
+            elif action == 'diff':
+                get_diff_options(req)
+                req.redirect(req.href.wiki(page.name, version=page.version,
+                                           action='diff'))
+        elif action == 'delete':
+            self._render_confirm(req, db, page)
+        elif action == 'edit':
+            self._render_editor(req, db, page)
+        elif action == 'diff':
+            self._render_diff(req, db, page)
+        elif action == 'history':
+            self._render_history(req, db, page)
+        else:
+            format = req.args.get('format')
+            if format:
+                Mimeview(self.env).send_converted(req, 'text/x-trac-wiki',
+                                                  page.text, format, page.name)
+            self._render_view(req, db, page)
+
+        req.hdf['wiki.action'] = action
+        req.hdf['wiki.current_href'] = req.href.wiki(page.name)
+        return 'wiki.cs', None
+
+    # ITimelineEventProvider methods
+
+    def get_timeline_filters(self, req):
+        if req.perm.has_permission('WIKI_VIEW'):
+            yield ('wiki', 'Wiki changes')
+
+    def get_timeline_events(self, req, start, stop, filters):
+        if 'wiki' in filters:
+            wiki = WikiSystem(self.env)
+            format = req.args.get('format')
+            href = format == 'rss' and req.abs_href or req.href
+            db = self.env.get_db_cnx()
+            cursor = db.cursor()
+            cursor.execute("SELECT time,name,comment,author,version "
+                           "FROM wiki WHERE time>=%s AND time<=%s",
+                           (start, stop))
+            for t,name,comment,author,version in cursor:
+                title = Markup('<em>%s</em> edited by %s',
+                               wiki.format_page_name(name), author)
+                diff_link = html.A('diff', href=href.wiki(name, action='diff',
+                                                          version=version))
+                if format == 'rss':
+                    comment = wiki_to_html(comment or '--', self.env, req, db,
+                                           absurls=True)
+                else:
+                    comment = wiki_to_oneliner(comment, self.env, db,
+                                               shorten=True)
+                if version > 1:
+                    comment = Markup('%s (%s)', comment, diff_link)
+                yield 'wiki', href.wiki(name), title, t, author, comment
+
+            # Attachments
+            def display(id):
+                return Markup('ticket ', html.EM('#', id))
+            att = AttachmentModule(self.env)
+            for event in att.get_timeline_events(req, db, 'wiki', format,
+                                                 start, stop,
+                                                 lambda id: html.EM(id)):
+                yield event
+
+    # Internal methods
+
+    def _set_title(self, req, page, action):
+        title = name = WikiSystem(self.env).format_page_name(page.name)
+        if action:
+            title += ' (%s)' % action
+        req.hdf['wiki.page_name'] = name
+        req.hdf['title'] = title
+        return title
+
+    def _do_delete(self, req, db, page):
+        if page.readonly:
+            req.perm.assert_permission('WIKI_ADMIN')
+        else:
+            req.perm.assert_permission('WIKI_DELETE')
+
+        if req.args.has_key('cancel'):
+            req.redirect(req.href.wiki(page.name))
+
+        version = int(req.args.get('version', 0)) or None
+        old_version = int(req.args.get('old_version', 0)) or version
+
+        if version and old_version and version > old_version:
+            # delete from `old_version` exclusive to `version` inclusive:
+            for v in range(old_version, version):
+                page.delete(v + 1, db)
+        else:
+            # only delete that `version`, or the whole page if `None`
+            page.delete(version, db)
+        db.commit()
+
+        if not page.exists:
+            req.redirect(req.href.wiki())
+        else:
+            req.redirect(req.href.wiki(page.name))
+
+    def _do_save(self, req, db, page):
+        if page.readonly:
+            req.perm.assert_permission('WIKI_ADMIN')
+        elif not page.exists:
+            req.perm.assert_permission('WIKI_CREATE')
+        else:
+            req.perm.assert_permission('WIKI_MODIFY')
+
+        page.text = req.args.get('text')
+        if req.perm.has_permission('WIKI_ADMIN'):
+            # Modify the read-only flag if it has been changed and the user is
+            # WIKI_ADMIN
+            page.readonly = int(req.args.has_key('readonly'))
+
+        # Give the manipulators a pass at post-processing the page
+        for manipulator in self.page_manipulators:
+            for field, message in manipulator.validate_wiki_page(req, page):
+                if field:
+                    raise InvalidWikiPage("The Wiki page field %s is invalid: %s"
+                                          % (field, message))
+                else:
+                    raise InvalidWikiPage("Invalid Wiki page: %s" % message)
+
+        page.save(get_reporter_id(req, 'author'), req.args.get('comment'),
+                  req.remote_addr)
+        req.redirect(req.href.wiki(page.name))
+
+    def _render_confirm(self, req, db, page):
+        if page.readonly:
+            req.perm.assert_permission('WIKI_ADMIN')
+        else:
+            req.perm.assert_permission('WIKI_DELETE')
+
+        version = None
+        if req.args.has_key('delete_version'):
+            version = int(req.args.get('version', 0))
+        old_version = int(req.args.get('old_version', 0)) or version
+
+        self._set_title(req, page, 'delete')
+        req.hdf['wiki'] = {'mode': 'delete'}
+        if version is not None:
+            num_versions = 0
+            for v,t,author,comment,ipnr in page.get_history():
+                if v >= old_version:
+                    num_versions += 1;
+                    if num_versions > 1:
+                        break
+            req.hdf['wiki'] = {'version': version, 'old_version': old_version,
+                               'only_version': num_versions == 1}
+
+    def _render_diff(self, req, db, page):
+        req.perm.assert_permission('WIKI_VIEW')
+
+        if not page.exists:
+            raise TracError("Version %s of page %s does not exist" %
+                            (req.args.get('version'), page.name))
+
+        add_stylesheet(req, 'common/css/diff.css')
+
+        self._set_title(req, page, 'diff')
+
+        # Ask web spiders to not index old versions
+        req.hdf['html.norobots'] = 1
+
+        old_version = req.args.get('old_version')
+        if old_version:
+            old_version = int(old_version)
+            if old_version == page.version:
+                old_version = None
+            elif old_version > page.version: # FIXME: what about reverse diffs?
+                old_version, page = page.version, \
+                                    WikiPage(self.env, page.name, old_version)
+        latest_page = WikiPage(self.env, page.name)
+        new_version = int(page.version)
+        info = {
+            'version': new_version,
+            'latest_version': latest_page.version,
+            'history_href': req.href.wiki(page.name, action='history')
+        }
+
+        num_changes = 0
+        old_page = None
+        prev_version = next_version = None
+        for version,t,author,comment,ipnr in latest_page.get_history():
+            if version == new_version:
+                if t:
+                    info['time'] = format_datetime(t)
+                    info['time_delta'] = pretty_timedelta(t)
+                info['author'] = author or 'anonymous'
+                info['comment'] = wiki_to_html(comment or '--',
+                                               self.env, req, db)
+                info['ipnr'] = ipnr or ''
+            else:
+                if version < new_version:
+                    num_changes += 1
+                    if not prev_version:
+                        prev_version = version
+                    if (old_version and version == old_version) or \
+                            not old_version:
+                        old_page = WikiPage(self.env, page.name, version)
+                        info['num_changes'] = num_changes
+                        info['old_version'] = version
+                        break
+                else:
+                    next_version = version
+        req.hdf['wiki'] = info
+
+        # -- prev/next links
+        if prev_version:
+            add_link(req, 'prev', req.href.wiki(page.name, action='diff',
+                                                version=prev_version),
+                     'Version %d' % prev_version)
+        if next_version:
+            add_link(req, 'next', req.href.wiki(page.name, action='diff',
+                                                version=next_version),
+                     'Version %d' % next_version)
+
+        # -- text diffs
+        diff_style, diff_options = get_diff_options(req)
+
+        oldtext = old_page and old_page.text.splitlines() or []
+        newtext = page.text.splitlines()
+        context = 3
+        for option in diff_options:
+            if option.startswith('-U'):
+                context = int(option[2:])
+                break
+        if context < 0:
+            context = None
+        changes = hdf_diff(oldtext, newtext, context=context,
+                           ignore_blank_lines='-B' in diff_options,
+                           ignore_case='-i' in diff_options,
+                           ignore_space_changes='-b' in diff_options)
+        req.hdf['wiki.diff'] = changes
+
+    def _render_editor(self, req, db, page, preview=False):
+        req.perm.assert_permission('WIKI_MODIFY')
+
+        if req.args.has_key('text'):
+            page.text = req.args.get('text')
+        if preview:
+            page.readonly = req.args.has_key('readonly')
+
+        author = get_reporter_id(req, 'author')
+        comment = req.args.get('comment', '')
+        editrows = req.args.get('editrows')
+        if editrows:
+            pref = req.session.get('wiki_editrows', '20')
+            if editrows != pref:
+                req.session['wiki_editrows'] = editrows
+        else:
+            editrows = req.session.get('wiki_editrows', '20')
+
+        self._set_title(req, page, 'edit')
+        info = {
+            'page_source': page.text,
+            'version': page.version,
+            'author': author,
+            'comment': comment,
+            'readonly': page.readonly,
+            'edit_rows': editrows,
+            'scroll_bar_pos': req.args.get('scroll_bar_pos', '')
+        }
+        if page.exists:
+            info['history_href'] = req.href.wiki(page.name,
+                                                 action='history')
+            info['last_change_href'] = req.href.wiki(page.name,
+                                                     action='diff',
+                                                     version=page.version)
+        if preview:
+            info['page_html'] = wiki_to_html(page.text, self.env, req, db)
+            info['comment_html'] = wiki_to_oneliner(comment, self.env, req, db)
+            info['readonly'] = int(req.args.has_key('readonly'))
+        req.hdf['wiki'] = info
+
+    def _render_history(self, req, db, page):
+        """Extract the complete history for a given page and stores it in the
+        HDF.
+
+        This information is used to present a changelog/history for a given
+        page.
+        """
+        req.perm.assert_permission('WIKI_VIEW')
+
+        if not page.exists:
+            raise TracError, "Page %s does not exist" % page.name
+
+        self._set_title(req, page, 'history')
+
+        history = []
+        for version, t, author, comment, ipnr in page.get_history():
+            history.append({
+                'url': req.href.wiki(page.name, version=version),
+                'diff_url': req.href.wiki(page.name, version=version,
+                                          action='diff'),
+                'version': version,
+                'time': format_datetime(t),
+                'time_delta': pretty_timedelta(t),
+                'author': author,
+                'comment': wiki_to_oneliner(comment or '', self.env, db),
+                'ipaddr': ipnr
+            })
+        req.hdf['wiki.history'] = history
+
+    def _render_view(self, req, db, page):
+        req.perm.assert_permission('WIKI_VIEW')
+
+        page_name = self._set_title(req, page, '')
+        if page.name == 'WikiStart':
+            req.hdf['title'] = ''
+
+        version = req.args.get('version')
+        if version:
+            # Ask web spiders to not index old versions
+            req.hdf['html.norobots'] = 1
+
+        # Add registered converters
+        for conversion in Mimeview(self.env).get_supported_conversions(
+                                             'text/x-trac-wiki'):
+            conversion_href = req.href.wiki(page.name, version=version,
+                                            format=conversion[0])
+            add_link(req, 'alternate', conversion_href, conversion[1],
+                     conversion[3])
+
+        latest_page = WikiPage(self.env, page.name)
+        req.hdf['wiki'] = {'exists': page.exists,
+                           'version': page.version,
+                           'latest_version': latest_page.version,
+                           'readonly': page.readonly}
+        if page.exists:
+            req.hdf['wiki'] = {
+                'page_html': wiki_to_html(page.text, self.env, req),
+                'history_href': req.href.wiki(page.name, action='history'),
+                'last_change_href': req.href.wiki(page.name, action='diff',
+                                                  version=page.version)
+                }
+            if version:
+                req.hdf['wiki'] = {
+                    'comment_html': wiki_to_oneliner(page.comment or '--',
+                                                     self.env, db),
+                    'author': page.author,
+                    'age': pretty_timedelta(page.time)
+                    }
+        else:
+            if not req.perm.has_permission('WIKI_CREATE'):
+                raise HTTPNotFound('Page %s not found', page.name)
+            req.hdf['wiki.page_html'] = html.P('Describe "%s" here' % page_name)
+
+        # Show attachments
+        req.hdf['wiki.attachments'] = attachments_to_hdf(self.env, req, db,
+                                                         'wiki', page.name)
+        if req.perm.has_permission('WIKI_MODIFY'):
+            attach_href = req.href.attachment('wiki', page.name)
+            req.hdf['wiki.attach_href'] = attach_href
+
+    # ISearchSource methods
+
+    def get_search_filters(self, req):
+        if req.perm.has_permission('WIKI_VIEW'):
+            yield ('wiki', 'Wiki')
+
+    def get_search_results(self, req, terms, filters):
+        if not 'wiki' in filters:
+            return
+        db = self.env.get_db_cnx()
+        sql_query, args = search_to_sql(db, ['w1.name', 'w1.author', 'w1.text'], terms)
+        cursor = db.cursor()
+        cursor.execute("SELECT w1.name,w1.time,w1.author,w1.text "
+                       "FROM wiki w1,"
+                       "(SELECT name,max(version) AS ver "
+                       "FROM wiki GROUP BY name) w2 "
+                       "WHERE w1.version = w2.ver AND w1.name = w2.name "
+                       "AND " + sql_query, args)
+
+        for name, date, author, text in cursor:
+            yield (req.href.wiki(name), '%s: %s' % (name, shorten_line(text)),
+                   date, author, shorten_result(text, terms))
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/CamelCase
@@ -0,0 +1,12 @@
+= !CamelCase =
+New words created by smashing together capitalized words.
+
+CamelCase is the original wiki convention for creating hyperlinks, with the additional requirement that the capitals are followed by a lower-case letter; hence “AlabamA†and “ABc†will not be links.
+
+== More information on !CamelCase ==
+
+ * http://c2.com/cgi/wiki?WikiCase
+ * http://en.wikipedia.org/wiki/CamelCase
+
+----
+See also: WikiPageNames, WikiNewPage, WikiFormatting, TracWiki
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/InterMapTxt
@@ -0,0 +1,64 @@
+= InterMapTxt =
+
+== This is the place for defining InterWiki prefixes ==
+
+This page was modelled after the MeatBall:InterMapTxt page.
+In addition, an optional comment is allowed after the mapping.
+
+
+This page is interpreted in a special way by Trac, in order to support
+!InterWiki links in a flexible and dynamic way.
+
+The code block after the first line separator in this page
+will be interpreted as a list of !InterWiki specifications:
+{{{
+prefix <space> URL [<space> # comment]
+}}}
+
+By using `$1`, `$2`, etc. within the URL, it is possible to create 
+InterWiki links which support multiple arguments, e.g. Trac:ticket:40.
+The URL itself can be optionally followed by a comment, 
+which will subsequently be used for decorating the links 
+using that prefix.
+
+New !InterWiki links can be created by adding to that list, in real time.
+Note however that ''deletions'' are also taken into account immediately,
+so it may be better to use comments for disabling prefixes.
+
+Also note that !InterWiki prefixes are case insensitive.
+
+
+== List of Active InterWiki Prefixes ==
+
+[[InterWiki]]
+
+----
+
+== Prefix Definitions ==
+
+{{{
+PEP     http://www.python.org/peps/pep-$1.html                                       # Python Enhancement Proposal 
+TracML  http://thread.gmane.org/gmane.comp.version-control.subversion.trac.general/  # Trac Mailing List
+
+#
+# A arbitrary pick of InterWiki prefixes...
+#
+Acronym          http://www.acronymfinder.com/af-query.asp?String=exact&Acronym=
+C2find           http://c2.com/cgi/wiki?FindPage&value=
+Cache            http://www.google.com/search?q=cache:
+CPAN             http://search.cpan.org/perldoc?
+DebianBug        http://bugs.debian.org/
+DebianPackage    http://packages.debian.org/
+Dictionary       http://www.dict.org/bin/Dict?Database=*&Form=Dict1&Strategy=*&Query=
+Google           http://www.google.com/search?q=
+GoogleGroups     http://groups.google.com/groups?q=
+JargonFile       http://downlode.org/perl/jargon-redirect.cgi?term=
+MeatBall         http://www.usemod.com/cgi-bin/mb.pl?
+MetaWiki         http://sunir.org/apps/meta.pl?
+MetaWikiPedia    http://meta.wikipedia.org/wiki/
+MoinMoin         http://moinmoin.wikiwikiweb.de/
+WhoIs            http://www.whois.sc/
+Why              http://clublet.com/c/c/why?
+Wiki             http://c2.com/cgi/wiki?
+WikiPedia        http://en.wikipedia.org/wiki/
+}}}
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/InterTrac
@@ -0,0 +1,82 @@
+= InterTrac Extension for TracLinks =
+
+''(since [milestone:0.10])''
+
+== Definitions ==
+
+An InterTrac link is used for referring to a Trac object 
+(Wiki page, changeset, ticket, ...) located in another
+Trac environment.
+
+== List of Active InterTrac Prefixes ==
+
+[[InterTrac]]
+
+== Link Syntax ==
+
+{{{
+<target_environment>:<TracLinks>
+}}}
+
+The link is composed by the target environment name, 
+followed by a colon (e.g. `trac:`),
+followed by a regular TracLinks, of any flavor.
+
+That target environment name is either the real name of the 
+environment, or an alias for it. 
+The aliases are defined in `trac.ini` (see below).
+The prefix is case insensitive.
+
+For convenience, there's also an alternative short-hand form, 
+where one can use an alias as an immediate prefix 
+for the identifier of a ticket, changeset or report:
+(e.g. `#T234`, `[T1508]`, `[trac 1508]`, ...)
+
+== Configuration ==
+
+It is necessary to setup a specific `[intertrac]` section in the TracIni for the InterTrac facility, in order to associate a prefix to other Trac sites, and for defining environment aliases.
+
+Example configuration:
+{{{
+...
+[intertrac]
+## -- Example of setting up an alias:
+t = trac
+
+## -- Link to an external Trac:
+trac.title = Edgewall's Trac for Trac
+trac.url = http://projects.edgewall.com/trac
+
+#trac.svn = http://repos.edgewall.com/projects/trac 
+# Hint: .svn information could be used in the future to support svn:externals...
+}}}
+
+Now, given this configuration, one could create the following links:
+ * to the current InterTrac page:
+   * `trac:wiki:InterTrac` ->
+     [http://projects.edgewall.com/trac/wiki/InterTrac trac:wiki:InterTrac]
+   * `t:wiki:InterTrac` ->
+     [http://projects.edgewall.com/trac/wiki/InterTrac t:wiki:InterTrac]
+   * Keys are case insensitive: `T:wiki:InterTrac` -> 
+     [http://projects.edgewall.com/trac/wiki/InterTrac T:wiki:InterTrac]
+ * to the ticket #234:
+   * `trac:ticket:234` ->
+     [http://projects.edgewall.com/trac/ticket/234 trac:ticket:234]
+   * `trac:#234` ->
+     [http://projects.edgewall.com/trac/ticket/234 trac:#234]
+   * `#T234` ->
+     [http://projects.edgewall.com/trac/search?q=#234 #T234]
+ * to the changeset [1912]:
+   * `trac:changeset:1912` ->
+     [http://projects.edgewall.com/trac/changeset/1912 trac:changeset:1912]
+   * `trac:[1912]` ->
+     [http:"//projects.edgewall.com/trac/search?q=[1912]" "trac:[1912]"]
+   * `[T1912]` ->
+     [http://projects.edgewall.com/trac/changeset/1912 "[T1912]"]
+
+Anything not given as explicit links (intertrac_prefix:module:id)
+is interpreted by the remote Trac, relying on its quickjump
+facility.
+
+----
+See also: TracLinks, InterWiki
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/InterWiki
@@ -0,0 +1,67 @@
+= Support for InterWiki links =
+
+''(since [milestone:0.10])''
+
+== Definition ==
+
+An InterWiki link can be used for referring to a Wiki page
+located in another Wiki system, and by extension, to any object
+located in any other Web application, provided a simple URL 
+mapping can be done.
+
+== Link Syntax ==
+
+{{{
+<target_wiki>(:<identifier>)+
+}}}
+
+The link is composed by the targeted Wiki (or system) name,
+followed by a column (e.g. {{{MeatBall:}}}),
+followed by a page specification in the target.
+Note that, as for InterTrac prefixes, InterWiki prefixes are case insensitive.
+
+The target Wiki URL is looked up in a the InterMapTxt wiki page, 
+modelled after
+[http://www.usemod.com/cgi-bin/mb.pl?InterMapTxt MeatBall:InterMapTxt].
+
+In addition to traditional InterWiki links, where the target
+is simply ''appended'' to the URL, 
+Trac supports parametric InterWiki URLs:
+identifiers `$1`, `$2`, ... in the URL
+will be replaced by corresponding arguments.
+The argument list is formed by splitting the page identifier
+using the ":" separator.
+
+== Examples ==
+
+If the following is an excerpt of the InterMapTxt page:
+
+{{{
+= InterMapTxt =
+== This is the place for defining InterWiki prefixes ==
+
+Currently active prefixes: [[InterWiki]]
+
+This page is modelled after the MeatBall:InterMapTxt page.
+In addition, an optional comment is allowed after the mapping.
+----
+{{{
+PEP     http://www.python.org/peps/pep-$1.html                                       # Python Enhancement Proposal $1 
+TracML  http://thread.gmane.org/gmane.comp.version-control.subversion.trac.general/$1  # Message $1 in Trac Mailing List
+
+...
+MeatBall http://www.usemod.com/cgi-bin/mb.pl?
+MetaWiki http://sunir.org/apps/meta.pl?
+MetaWikiPedia http://meta.wikipedia.org/wiki/
+MoinMoin http://moinmoin.wikiwikiweb.de/
+...
+}}}
+}}}
+
+Then, 
+ * `MoinMoin:InterWikiMap` should be rendered as 
+   [http://moinmoin.wikiwikiweb.de/InterWikiMap MoinMoin:InterWikiMap]
+   and the ''title'' for that link would be "!InterWikiMap in !MoinMoin"
+ * {{{TracML:4346}}} should be rendered as 
+   [http://thread.gmane.org/gmane.comp.version-control.subversion.trac.general/4346 TracML:4346]
+   and the ''title'' for that link would be "Message 4346 in Trac Mailing List"
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/RecentChanges
@@ -0,0 +1,3 @@
+= Recent Changes =
+
+[[RecentChanges]]
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/SandBox
@@ -0,0 +1,5 @@
+= The Sandbox =
+
+This is just a page to practice and learn WikiFormatting. 
+
+Go ahead, edit it freely.
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TitleIndex
@@ -0,0 +1,3 @@
+= Title Index =
+
+[[TitleIndex]]
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracAccessibility
@@ -0,0 +1,21 @@
+= Accessibility Support in Trac =
+
+Not every user has a graphic environment with a mouse or other pointing device. Some users rely on keyboard, alternative keyboard or voice input to navigate links, activate form controls, etc. In Trac, we work to assure users may interact with devices other than a pointing device.
+
+Trac supports accessibility keys for the most common operations. On Windows and Linux platforms, press any of the keys listed below in combination with the `<Alt>` key; on a Mac, use the `<ctrl>` key instead.
+
+''Note that when using Internet Explorer on Windows, you need to hit enter after having used the access key.''
+
+== Global Access Keys ==
+
+ * `1` - WikiStart
+ * `2` - [wiki:TracTimeline Timeline]
+ * `3` - [wiki:TracRoadmap Roadmap]
+ * `4` - [wiki:TracSearch Search]
+ * `6` - [wiki:TracGuide Trac Guide / Documentation]
+ * `7` - [wiki:TracTickets New Ticket]
+ * `9` - [../about About Trac]
+ * `0` - This page
+
+----
+See also: TracGuide
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracAdmin
@@ -0,0 +1,37 @@
+= TracAdmin =
+[[TracGuideToc]]
+
+Trac is distributed with a powerful command-line configuration tool. This tool can be used  to configure and customize your Trac-installation to better fit your needs.
+
+== Usage ==
+
+You can get a comprehensive list of the available options, commands and sub-commands by invoking `trac-admin` with the `help` command:
+{{{
+trac-admin help
+}}}
+
+Unless you're executing the `help`, `about` or `version` sub-commands, you'll need to specify the path to the TracEnvironment that you want to administer as the first argument, for example:
+{{{
+trac-admin /path/to/projenv wiki list
+}}}
+
+== Interactive Mode ==
+
+When passing the environment path as the only argument, `trac-admin` starts in interactive mode.
+Commands can then be executed on the selected environment using the prompt, which offers tab-completion
+(on non-Windows environments, and when the Python `readline` module is available) and automatic repetition of the last command issued.
+
+Once you're in interactive mode, you can also get help on specific commands or subsets of commands:
+
+For example, to get an explanation of the `resync` command, run:
+{{{
+> help resync
+}}}
+
+To get help on a all the Wiki-related commands, run:
+{{{
+> help wiki
+}}}
+
+----
+See also: TracGuide, TracBackup, TracPermissions, TracEnvironment, TracIni
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracBackup
@@ -0,0 +1,28 @@
+= Trac Backup =
+[[TracGuideToc]]
+
+Since Trac uses a database backend, some extra care is required to safely create a backup of a [wiki:TracEnvironment project environment]. Luckily, [wiki:TracAdmin trac-admin] has a command to make backups easier: `hotcopy`.
+
+  ''Note: Trac uses the `hotcopy` nomenclature to match that of [http://subversion.tigris.org/ Subversion], to make it easier to remember when managing both Trac and Subversion servers.''
+
+== Creating a Backup ==
+
+To create a backup of a live TracEnvironment, simply run:
+{{{
+  $ trac-admin /path/to/projenv hotcopy /path/to/backupdir
+}}}
+
+[wiki:TracAdmin trac-admin] will lock the database while copying.''
+
+The resulting backup directory is safe to handle using standard file-based backup tools like `tar` or `dump`/`restore`.
+
+=== Restoring a Backup ===
+
+Backups are simply a copied snapshot of the entire [wiki:TracEnvironment project environment] directory, including the SQLite database. 
+
+To restore an environment from a backup, simply stop the process running Trac (i.e. the Web server or [wiki:TracStandalone tracd]), restore the directory structure from the backup and restart the service.
+
+  ''Note: Automatic backup of environments that don't use SQLite as database backend is not supported at this time. As a workaround, we recommend that you stop the server, copy the environment directory, and make a backup of the database using whatever mechanism is provided by the database system.''
+
+----
+See also: TracAdmin, TracEnvironment, TracGuide
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracBrowser
@@ -0,0 +1,30 @@
+= The Trac Browser =
+[[TracGuideToc]]
+
+The Trac browser can be used to browse directories, change logs 
+and specific revisions of files stored in a subversion repository.
+
+Directory entries are displayed in a list with sortable columns. The list 
+entries can be sorted by ''name'', ''size'' or ''age'' by clicking on the column
+headers. The sort order can be reversed by clicking on a given column
+header again.
+
+The browser can be used to navigate through the directory structure 
+by clicking on the directory names. Clicking on a file name will show
+the contents of the file. Clicking on the revision number of a file or
+directory will take you to the revision history for that file.
+
+It's also possible to browse directories or files as they were in history,
+at any given repository revision. The default behavior is to display the
+latest revision but another revision number can easily be selected using
+the ''View revision'' input field at the top of the page.
+
+== RSS Support ==
+
+The browser module provided RSS feeds to monitor changes to a file or
+directory. To subscribe to an RSS feed for a file or directory, open its
+revision log in the browser and click the orange 'XML' icon at the bottom
+of the page. For more information on RSS support in Trac, see TracRss.
+
+----
+See also: TracGuide, TracChangeset, FineGrainedPermissions
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracCgi
@@ -0,0 +1,88 @@
+= Installing Trac as CGI =
+
+To install Trac as a CGI script, you need to make the `trac.cgi` executable as a CGI by your web server. If you're using [http://httpd.apache.org/ Apache HTTPD], there are a couple ways to do that:
+
+ 1. Use a `ScriptAlias` to map a URL to the `trac.cgi` script
+ 2. Copy the `trac.cgi` file into the directory for CGI executables used by your web server (commonly named `cgi-bin`). You can also create a symbolic link, but in that case make sure that the `FollowSymLinks` option is enabled for the `cgi-bin` directory.
+
+The first option is recommended as it also allows you to map the CGI to a friendly URL.
+
+Now, edit the Apache configuration file and add this snippet, file names and locations changed to match your installation:
+{{{
+ScriptAlias /trac /usr/share/trac/cgi-bin/trac.cgi
+
+# Trac needs to know where the database is located
+<Location "/trac">
+  SetEnv TRAC_ENV "/path/to/projectenv"
+</Location>
+}}}
+
+This will make Trac available at `http://yourhost.example.org/trac`.
+
+ ''Note: Make sure that the modules mod_alias and mod_env modules are available and enabled in your Apache configuration, otherwise Apache will complain about the above snippet.''
+
+ ''Note: If you are using the [http://httpd.apache.org/docs/suexec.html Apache suEXEC] feature see [http://projects.edgewall.com/trac/wiki/ApacheSuexec ApacheSuexec] (on the main Trac site).''
+
+== Mapping Static Resources ==
+
+Out of the box, Trac will serve static resources such as style sheets or images itself. For a CGI setup, though, this is highly undesirable, because it results in the CGI script being invoked for documents that could be more efficiently served by the web server.
+
+Web servers such as [http://httpd.apache.org/ Apache HTTPD] allow you to create “Aliases†to resources, thereby giving them a virtual URL that doesn't necessarily bear any resemblance to the layout of the servers file system. We already used this capability above when defining a `ScriptAlias` for the CGI script, and we'll use it now to map requests to the static resources to the directory on the file system that contains them, thereby bypassing the processing of such requests by the CGI script.
+
+Edit the Apache configuration file again and add the following snippet '''before''' the `ScriptAlias` for the CGI script , file names and locations changed to match your installation:
+{{{
+Alias /trac/chrome/common /usr/share/trac/htdocs
+<Directory "/usr/share/trac/htdocs">
+  Order allow,deny
+  Allow from all
+</Directory>
+}}}
+
+Note that whatever URL path you mapped the `trac.cgi` script to, the path `/chrome/common` is the path you have to append to that location to intercept requests to the static resources. 
+
+For example, if Trac is mapped to `/cgi-bin/trac.cgi` on your server, the URL of the Alias should be `/cgi-bin/trac.cgi/chrome/common`.
+
+Alternatively, you can set the `htdocs_location` configuration option in [wiki:TracIni trac.ini]:
+{{{
+[trac]
+htdocs_location = /trac-htdocs
+}}}
+
+Trac will then use this URL when embedding static resources into HTML pages. Of course, you still need to make the Trac `htdocs` directory available through the web server at the specified URL, for example by copying (or linking) the directory into the document root of the web server.
+
+== Adding Authentication ==
+
+The simplest way to enable authentication with Apache is to create a password file. Use the `htpasswd` program to create the password file:
+{{{
+$ htpasswd -c /somewhere/trac.htpasswd admin
+New password: <type password>
+Re-type new password: <type password again>
+Adding password for user admin
+}}}
+
+After the first user, you dont need the "-c" option anymore:
+{{{
+$ htpasswd /somewhere/trac.htpasswd john
+New password: <type password>
+Re-type new password: <type password again>
+Adding password for user john
+}}}
+
+  ''See the man page for `htpasswd` for full documentation.''
+
+After you've created the users, you can set their permissions using TracPermissions.
+
+Now, you'll need to enable authentication against the password file in the Apache configuration:
+{{{
+<Location "/cgi-bin/trac.cgi/login">
+  AuthType Basic
+  AuthName "Trac"
+  AuthUserFile /somewhere/trac.htpasswd
+  Require valid-user
+</Location>
+}}}
+
+For better security, it is recommended that you either enable SSL or at least use the “Digest†authentication scheme instead of “Basicâ€. Please read the [http://httpd.apache.org/docs/2.0/ Apache HTTPD documentation] to find out more.
+
+----
+See also:  TracGuide, TracInstall, TracFastCgi, TracModPython
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracChangeset
@@ -0,0 +1,118 @@
+= Trac Changeset Module =
+[[TracGuideToc]]
+
+Trac has a built-in functionality for visualizing “diffs†- changes to files.
+
+There are different kinds of ''change sets''. 
+Some can correspond to revisions made in the repositories,
+others can aggregate changes made in several revisions, 
+but in the end, any kind of differences could be shown.
+
+The changeset view consists of two parts, the ''header'' 
+and the ''diff views''.
+
+== Changeset Header ==
+
+The header shows an overview of the whole changeset.
+Here you will find information such as:
+
+ * Timestamp -- When the changeset was commited
+ * Author -- Who commited the changeset
+ * Message -- A brief description from the author (the commit log message)
+ * Files -- A list of files affected by this changeset
+
+If more than one revision is involved in the set of changes being
+displayed, the ''Timestamp'', ''Author'' and ''Message'' fields 
+won't be shown.
+
+In front of each listed file, you'll find  a colored rectangle. The color
+indicates how the file is affected by the changeset.
+ 
+ * Green: Added
+ * Red: Removed
+ * Yellow: Modified
+ * Blue: Copied
+ * Gray: Moved
+
+The color legend is located below the header as a reminder.
+
+== Diff Views ==
+
+Below the header is the main part of the changeset, the diff view. Each file is shown in a separate section, each of which will contain only the regions of the file that are affected by the changeset. There are two different styles of displaying the diffs: ''inline'' or ''side-by-side'' (you can switch between those styles using the preferences form):
+
+ * The ''inline'' style shows the changed regions of a file underneath each other. A region removed from the file will be colored red, an added region will be colored green. If a region was modified, the old version is displayed above the new version. Line numbers on the left side indicate the exact position of the change in both the old and the new version of the file.
+ * The ''side-by-side'' style shows the old version on the left and the new version on the right (this will typically require more screen width than the inline style.) Added and removed regions will be colored in the same way as with the inline style (green and red, respectively), but modified regions will have a yellow background.
+
+In addition, various advanced options are available in the preferences form for adjusting the display of the diffs:
+ * You can set how many lines are displayed before and after every change
+   (if the value ''all'' is used, then the full file will be shown)
+ * You can toggle whether blank lines, case changes and white space changes are ignored, thereby letting you find the functional changes more quickly
+
+
+== The Different Ways to Get a Diff ==
+
+=== Examining a Changeset ===
+
+When viewing a repository check-in, such as when following a
+changeset [wiki:TracLinks link] or a changeset event in the 
+[wiki:TracTimeline timeline], Trac will display the exact changes
+made by the check-in.
+
+There will be also navigation links to the ''Previous Changeset''
+to and ''Next Changeset''.
+
+
+'''Note: all of the following will only be available in Trac [milestone:0.10]'''
+
+=== Examining Differences Between Revisions ===
+
+A very frequent need is to look at changes made on a file 
+or on a directory spanning multiple revisions. 
+The easiest way to get there is from the TracRevisionLog, 
+where one can select the '''old''' and the '''new''' revisions
+of the path being examined, and then click the ''View changes''
+button.
+
+=== Examining Arbitrary Differences ===
+
+One of the main feature of source configuration management
+systems is the possibility to work simultaneously on alternate
+''Lines of Developments'', or ''branches''. 
+The evolution of branches are often made in parallel, making it
+sometimes difficult to understand the exact set of differences 
+between alternative versions.
+
+This is where Trac comes to the rescue: 
+the '''View changes ...''' button in the TracBrowser
+leads to a form permitting the selection of arbitrary
+''From:'' and ''To:'' path/revision pairs.
+
+The resulting set of differences consist in the changes 
+that should be applied to the ''From:'' content in order
+to make it look like the ''To:'' content.
+
+For convenience, it is possible to invert the roles
+of the '''old''' and the '''new''' path/revision pairs
+by clicking the ''Reverse Diff'' link on the changeset page.
+
+=== Checking the Last Change ===
+
+The last possibility for looking at changes is 
+to have a quick look on the ''Last Change'' while
+browsing a file or a directory. 
+
+This shows the last change that happened on that path.
+The links ''Previous Changeset'' and ''Next Changeset''
+are replace by links to ''Previous Change'' and ''Next Change'',
+which makes it really convenient to traverse the change history
+of a specific file or directory.
+This view of a changeset, restricted to a specific path,
+is called ''restricted changeset''.
+
+Of course, if one is doing that on the root of the
+repository, there will be no path restriction
+and the full changeset will be shown.
+
+
+----
+See also: TracGuide, TracBrowser
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracEnvironment
@@ -0,0 +1,70 @@
+= Trac Storage - The Environment =
+
+Trac uses a directory structure and a database for storing project data.
+
+== Creating an Environment ==
+
+A new Trac environment is created using [wiki:TracAdmin trac-admin]:
+{{{
+$ trac-admin /path/to/projectenv initenv
+}}}
+
+[wiki:TracAdmin trac-admin] will ask you for the name of the project, the
+database connection string (explained below), and where your subversion
+repository is located.
+
+  ''Note: The web server user will require file system write permission to
+the environment directory and all the files inside. Please remember to set
+the appropriate permissions. The same applies to the Subversion
+repository, although Trac will only require read access as long as you're
+not using the BDB file system.''
+
+== Database Connection Strings ==
+
+Since version 0.9, Trac supports both [http://sqlite.org/ SQLite] and
+[http://www.postgresql.org/ PostgreSQL] as database backends.  The default
+is to use SQLite, which is probably sufficient for most projects. The database file
+is then stored in the environment directory, and can easily be
+[wiki:TracBackup backed up] together with the rest of the environment.
+
+The connection string for an embedded SQLite database is:
+{{{
+sqlite:db/trac.db
+}}}
+
+If you want to use PostgreSQL instead, you'll have to use a different
+connection string. For example, to connect to a database on the same
+machine called `trac`, that allows access to the user `johndoe` with
+the password `letmein`, use:
+{{{
+postgres://johndoe:letmein@localhost/trac
+}}}
+
+If PostgreSQL is running on a non-standard port (for example 9342), use:
+{{{
+postgres://johndoe:letmein@localhost:9342/trac
+}}}
+
+Note that with PostgreSQL you will have to create the database before running
+`trac-admin initenv`.
+
+== Directory Structure ==
+
+An environment directory will usually consist of the following files and directories:
+
+ * `README` - Brief description of the environment.
+ * `VERSION` - Contains the environment version identifier.
+ * `attachments` - Attachments to wiki pages and tickets are stored here.
+ * `conf`
+   * `trac.ini` - Main configuration file. See TracIni.
+ * `db`
+   * `trac.db` - The SQLite database (if you're using SQLite).
+ * `plugins` - Environment-specific [wiki:TracPlugins plugins] (Python eggs)
+ * `templates` - Custom environment-specific templates.
+   * `site_css.cs` - Custom CSS rules.
+   * `site_footer.cs` - Custom page footer.
+   * `site_header.cs` - Custom page header.
+ * `wiki-macros` - Environment-specific [wiki:WikiMacros Wiki macros].
+
+----
+See also: TracAdmin, TracBackup, TracIni, TracGuide
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracFastCgi
@@ -0,0 +1,57 @@
+= Trac with FastCGI =
+
+Since version 0.9, Trac supports being run through the [http://www.fastcgi.com/ FastCGI] interface. Like [wiki:TracModPython mod_python], this allows Trac to remain resident, and is faster than external CGI interfaces which must start a new process for each request. However, unlike mod_python, it is able to support [http://httpd.apache.org/docs/suexec.html SuEXEC]. Additionally, it is supported by much wider variety of web servers.
+
+== Simple Apache configuration ==
+{{{
+# Enable fastcgi for .fcgi files
+# (If you're using a distro package for mod_fcgi, something like
+# this is probably already present)
+<IfModule mod_fastcgi.c>
+   AddHandler fastcgi-script .fcgi
+   FastCgiIpcDir /var/lib/apache2/fastcgi 
+</IfModule>
+LoadModule fastcgi_module /usr/lib/apache2/modules/mod_fastcgi.so
+}}}
+
+You can either setup the `TRAC_ENV` as an overall default:
+{{{
+FastCgiConfig -initial-env TRAC_ENV=/path/to/env/trac
+}}}
+
+Or you can serve multiple Trac projects in a directory like:
+{{{
+FastCgiConfig -initial-env TRAC_ENV_PARENT_DIR=/parent/dir/of/projects
+}}}
+
+Configure `ScriptAlias` or similar options as described in TracCgi, but calling `trac.fcgi` instead of `trac.cgi`.
+
+== Simple Lighttpd Configuration ==
+
+The FastCGI front-end was developed primarily for use with alternative webservers, such as [http://www.lighttpd.net/ lighttpd].
+
+lighttpd is a secure, fast, compliant and very flexible web-server that has been optimized for high-performance
+environments.  It has a very low memory footprint compared to other web servers and takes care of CPU load.
+
+For using `trac.fcgi` with lighttpd add the following to your lighttpd.conf:
+{{{
+fastcgi.server = ("/trac" =>
+                   ("trac" =>
+                     ("socket" => "/tmp/trac-fastcgi.sock",
+                      "bin-path" => "/path/to/cgi-bin/trac.fcgi",
+                      "check-local" => "disable",
+                      "bin-environment" =>
+                        ("TRAC_ENV" => "/path/to/projenv")
+                     )
+                   )
+                 )
+}}}
+
+Note that you will need to add a new entry to `fastcgi.server` for each separate Trac instance that you wish to run. Alternatively, you may use the `TRAC_ENV_PARENT_DIR` variable instead of `TRAC_ENV` as described  above.
+
+Other important information like [http://trac.lighttpd.net/trac/wiki/TracInstall this updated TracInstall page], [wiki:TracCgi#MappingStaticResources and this] are useful for non-fastcgi specific installation aspects.
+
+Relaunch lighttpd, and browse to `http://yourhost.example.org/trac` to access Trac.
+
+----
+See also TracCgi, TracModPython, TracInstall, TracGuide
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracGuide
@@ -0,0 +1,34 @@
+= The Trac User and Administration Guide =
+[[TracGuideToc]]
+
+The TracGuide is meant to serve as a starting point for all documentation regarding Trac usage and development. The guide is a free document, a collaborative effort, and a part of the [http://projects.edgewall.com/trac/ Trac Project] itself.
+
+== Table of Contents ==
+Currently available documentation:
+ * TracGuide (This page)  -- Documentation starting point.
+   * TracInstall -- How to install and run Trac.
+   * TracUpgrade -- How to upgrade existing installations.
+   * TracAdmin -- Administrating a Trac project.
+   * TracImport -- Importing tickets from other bug databases.
+   * TracIni -- Trac configuration file reference. 
+   * TracPermissions -- Access control and permissions.
+   * TracInterfaceCustomization -- Customizing the Trac interface.
+   * TracPlugins -- Installing and managing Trac extensions.
+   * TracWiki -- How to use the built-in Wiki.
+   * TracBrowser -- Browsing source code with Trac.
+   * TracChangeset -- Viewing changes to source code.
+   * TracTickets -- Using the issue tracker.
+   * TracReports -- Writing and using reports.
+   * TracQuery -- Executing custom ticket queries.
+   * TracRoadmap -- The roadmap helps tracking project progress.
+   * TracTimeline -- The timeline provides a historic perspective on a project.
+   * TracLogging -- The Trac logging facility.
+   * TracRss -- RSS content syndication in Trac.
+   * TracNotification -- Email notification.
+
+ * [http://projects.edgewall.com/trac/wiki/TracFaq Trac FAQ] - A collection of Frequently Asked Questions (on the project website)
+
+== Support and Other Sources of Information ==
+If you are looking for a good place to ask a question about Trac, look no further than the [http://projects.edgewall.com/trac/wiki/MailingList MailingList]. It provides a friendly environment to discuss openly among Trac users and developers.
+
+See also the TracSupport page for more information resources.
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracImport
@@ -0,0 +1,77 @@
+= Importing ticket data =
+
+== Bugzilla ==
+
+Ticket data can be imported from Bugzilla using the [http://projects.edgewall.com/trac/browser/trunk/contrib/bugzilla2trac.py bugzilla2trac.py] script, available in the contrib/ directory of the Trac distribution.
+
+{{{
+$ bugzilla2trac.py
+bugzilla2trac - Imports a bug database from Bugzilla into Trac.
+
+Usage: bugzilla2trac.py [options]
+
+Available Options:
+  --db <MySQL dbname>              - Bugzilla's database
+  --tracenv /path/to/trac/env      - full path to Trac db environment
+  -h | --host <MySQL hostname>     - Bugzilla's DNS host name
+  -u | --user <MySQL username>     - effective Bugzilla's database user
+  -p | --passwd <MySQL password>   - Bugzilla's user password
+  -c | --clean                     - remove current Trac tickets before importing
+  --help | help                    - this help info
+
+Additional configuration options can be defined directly in the script.
+}}}
+
+Currently, the following data is imported from Bugzilla:
+
+  * bugs
+  * bug activity (field changes)
+  * bug attachments
+  * user names and passwords (put into a htpasswd file)
+
+The script provides a number of features to ease the conversion, such as:
+
+  * PRODUCT_KEYWORDS:  Trac doesn't have the concept of products, so the script provides the ability to attach a ticket keyword instead.
+
+  * IGNORE_COMMENTS:  Don't import Bugzilla comments that match a certain regexp.
+
+  * STATUS_KEYWORDS:  Attach ticket keywords for the Bugzilla statuses not available in Trac.  By default, the 'VERIFIED' and 'RELEASED' Bugzilla statuses are translated into Trac keywords.
+
+For more details on the available options, see the configuration section at the top of the script.
+
+== Sourceforge ==
+
+Ticket data can be imported from Sourceforge using the [http://projects.edgewall.com/trac/browser/trunk/contrib/sourceforge2trac.py sourceforge2trac.py] script, available in the contrib/ directory of the Trac distribution.
+
+== Mantis ==
+
+Mantis bugs can be imported using the attached script.
+
+Currently, the following data is imported from Mantis:
+  * bugs
+  * bug comments
+  * bug activity (field changes)
+
+Attachments are NOT imported.  If you use the script, please read the NOTES section (at the top of the file) and make sure you adjust the config parameters for your environment.
+
+mantis2trac.py has the same parameters as the bugzilla2trac.py script:
+{{{
+mantis2trac - Imports a bug database from Mantis into Trac.
+
+Usage: mantis2trac.py [options] 
+
+Available Options:
+  --db <MySQL dbname>              - Mantis database
+  --tracenv /path/to/trac/env      - Full path to Trac db environment
+  -h | --host <MySQL hostname>     - Mantis DNS host name
+  -u | --user <MySQL username>     - Effective Mantis database user
+  -p | --passwd <MySQL password>   - Mantis database user password
+  -c | --clean                     - Remove current Trac tickets before importing
+  --help | help                    - This help info
+
+Additional configuration options can be defined directly in the script.
+}}} 
+
+== Other ==
+
+Since trac uses a SQL database to store the data, you can import from other systems by examining the database tables. Just go into [http://www.sqlite.org/sqlite.html sqlite] command line to look at the tables and import into them from your application.
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracIni
@@ -0,0 +1,42 @@
+= The Trac Configuration File =
+[[TracGuideToc]]
+
+Trac configuration is done by editing the '''`trac.ini`''' config file, located in `<projectenv>/conf/trac.ini`.
+
+== Global Configuration ==
+
+Since version 0.9, Trac can also read the configuration from a global `trac.ini` file. These global options will then be merged with the environment-specific options, where local options override global options.
+
+The global configuration is by default localted in `$prefix/share/trac/conf/trac.ini`. It can be moved to a different location (for example, `/etc/trac.ini`), but that requires changing the file `trac/siteconfig.py` which gets created when Trac is installed. 
+
+== Reference ==
+
+This is a brief reference of available configuration options.
+
+[[TracIni]]
+
+== [components] ==
+(''since 0.9'')
+
+This section is used to enable or disable components provided by plugins, as well as by Trac itself. The component to enable/disable is specified via the name of the option. Whether its enabled is determined by the option value; setting the value to `enabled` or `on` will enable the component, any other value (typically `disabled` or `off`) will disable the component.
+
+The option name is either the fully qualified name of the components or the module/package prefix of the component. The former enables/disables a specific component, while the latter enables/disables any component in the specified package/module.
+
+Consider the following configuration snippet:
+{{{
+[components]
+trac.ticket.report.ReportModule = disabled
+webadmin.* = enabled
+}}}
+
+The first option tells Trac to disable the [wiki:TracReports report module]. The second option instructs Trac to enable all components in the `webadmin` package. Note that the trailing wildcard is required for module/package matching.
+
+See the ''Plugins'' page on ''About Trac'' to get the list of active components (requires `CONFIG_VIEW` [wiki:TracPermissions permissions].)
+
+See also: TracPlugins
+
+  ''Note that prior to Trac r2335 (that applies to 0.9b1 and 0.9b2), you would use a `[disabled_components]` section instead. See a [http://projects.edgewall.com/trac/wiki/TracIni?version=42 previous version] of this page for the details.''
+
+----
+See also: TracGuide, TracAdmin, TracEnvironment
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracInstall
@@ -0,0 +1,123 @@
+= Trac Installation Guide = 
+[[TracGuideToc]]
+
+Trac is a lightweight project management tool that is implemented as a web-based application. Trac is written in the Python programming language and can use [http://sqlite.org/ SQLite] or [http://www.postgresql.org/ PostgreSQL] as  database. For HTML rendering, Trac uses the [http://www.clearsilver.net/ Clearsilver] templating system.
+
+What follows are generic instructions for installing and setting up Trac and its requirements. While you can find instructions for installing Trac on specific systems at [http://projects.edgewall.com/trac/wiki/TracInstallPlatforms TracInstallPlatforms] on the main Trac site, please be sure to first read through these general instructions to get a good understanding of the tasks involved.
+
+== Requirements ==
+
+To install Trac, the following software packages must be installed:
+
+ * [http://www.python.org/ Python], version >= 2.3.
+   * Python 2.4 is not supported on Windows since there are no Subversion bindings available for it.
+   * For RPM-based systems you might also need the `python-devel` and `python-xml` packages.
+ * [http://subversion.tigris.org/ Subversion], version >= 1.0. (>= 1.1 recommended) and corresponding [http://svnbook.red-bean.com/svnbook-1.1/ch08s02.html#svn-ch-8-sect-2.3 Python bindings]
+   * Trac uses the [http://www.swig.org/ SWIG] bindings included in the Subversion distribution, '''not''' [http://pysvn.tigris.org/ PySVN] (which is sometimes confused with the standard SWIG bindings).
+   * If Subversion was already installed without the SWIG bindings, you'll need to re-`configure` Subversion and `make swig-py`, `make install-swig-py`.
+ * [http://www.clearsilver.net/ ClearSilver], version >= 0.9.3
+   * With python-bindings (`./configure --with-python=/usr/bin/python`)
+
+=== For SQLite ===
+
+ * [http://www.sqlite.org/ SQLite], version 2.8.x or 3.x
+ * [http://pysqlite.org/ PySQLite]
+   * version 1.0.x (for SQLite 2.8.x)
+   * version 1.1.x or 2.x (for SQLite 3.x)
+
+=== For PostgreSQL ===
+
+ * [http://www.postgresql.org/ PostgreSQL]
+ * [http://initd.org/projects/psycopg1 psycopg1], [http://initd.org/projects/psycopg2 psycopg2], or [http://pypgsql.sourceforge.net/ pyPgSQL]
+
+=== Optional Requirements ===
+
+ * A CGI-capable web server (see TracCgi), or
+ * a [http://www.fastcgi.com/ FastCGI]-capable web server (see TracFastCgi), or
+ * [http://httpd.apache.org/ Apache] with [http://www.modpython.org/ mod_python 3.1.3+] (see TracModPython)
+ * [http://peak.telecommunity.com/DevCenter/setuptools setuptools], version >= 0.5a13 for using plugins (see TracPlugins)
+ * [http://docutils.sourceforge.net/ docutils], version >= 0.3.3 for WikiRestructuredText.
+ * [http://silvercity.sourceforge.net/ SilverCity] and/or [http://www.gnu.org/software/enscript/enscript.html Enscript] for [wiki:TracSyntaxColoring syntax highlighting].
+
+'''Attention''': The various available versions of these dependencies are not necessarily interchangable, so please pay attention to the version numbers above. If you are having trouble getting Trac to work please double-check all the dependencies before asking for help on the [http://projects.edgewall.com/trac/wiki/MailingList MailingList] or [http://projects.edgewall.com/trac/wiki/IrcChannel IrcChannel].
+
+Please refer to the documentation of these packages to find out how they are best installed. In addition, most of the [http://projects.edgewall.com/trac/wiki/TracInstallPlatforms platform-specific instructions] also describe the installation of the dependencies.
+
+== Installing Trac ==
+
+Like most Python programs, the Trac Python package is installed by running the following command at the top of the source directory:
+{{{
+$ python ./setup.py install
+}}}
+
+''Note: you'll need root permissions or equivalent for this step.''
+
+This will byte-compile the python source code and install it in the `site-packages` directory
+of your Python installation. The directories `cgi-bin`, `templates`, `htdocs`, `wiki-default` and `wiki-macros` are all copied to `$prefix/share/trac/.`
+
+The script will also install the [wiki:TracAdmin trac-admin] command-line tool, used to create and maintain [wiki:TracEnvironment project environments], as well as the [wiki:TracStandalone tracd] standalone server.
+
+=== Advanced Users ===
+
+To install Trac to a custom location, or find out about other advanced installation options, run:
+{{{
+$ python ./setup.py --help
+}}}
+
+Specifically, you might be interested in:
+{{{
+$ python ./setup.py install --prefix=/path/you/want
+}}}
+
+
+== Creating a Project Environment ==
+
+A [wiki:TracEnvironment Trac environment] is the backend storage where Trac stores information like wiki pages, tickets, reports, settings, etc. An environment is basically a directory that contains a human-readable configuration file and various other files and directories.
+
+A new environment is created using [wiki:TracAdmin trac-admin]:
+{{{
+$ trac-admin /path/to/trac_project_env initenv
+}}}
+
+[wiki:TracAdmin trac-admin] will prompt you for the information it needs to create the environment, such as the name of the project, the path to an existing subversion repository, the [wiki:TracEnvironment#DatabaseConnectionStrings database connection string], and so on. If you're not sure what to specify for one of these options, just leave it blank to use the default value. The database connection string in particular will always work as long as you have SQLite installed. The only option where the default value is likely to not work is the path to the Subversion repository, so make sure that one's correct.
+
+Also note that the values you specify here can be changed later by directly editing the [wiki:TracIni] configuration file.
+
+''Note: The user account under which the web server runs will require write permissions to the environment
+directory and all the files inside.''
+
+
+== Running the Standalone Server ==
+
+After having created a Trac environment, you can easily try the web interface by running the standalone server [wiki:TracStandalone tracd]:
+{{{
+$ tracd --port 8000 /path/to/projectenv
+}}}
+
+Then, fire up a browser and visit `http://localhost:8000/`. You should get a simple listing of all environments that tracd knows about. Follow the link to the environment you just created, and you should see Trac in action.
+
+
+== Running Trac on a Web Server ==
+
+Trac provides three options for connecting to a “real†web server: [wiki:TracCgi CGI], [wiki:TracFastCgi FastCGI] and [wiki:TracModPython mod_python]. For decent performance, it is recommended that you use either FastCGI or mod_python.
+
+== Configuring Authentication ==
+
+The process of adding, removing, and configuring user accounts for authentication depends on the specific way you run Trac.  To learn about how to accomplish these tasks, please visit one of the following pages:
+
+ * TracStandalone if you use the standalone server, `tracd`.
+ * TracCgi if you use the CGI or FastCGI methods.
+ * TracModPython if you use the mod_python method.
+
+== Using Trac ==
+
+Once you have your Trac site up and running, you should be able to browse your subversion repository, create tickets, view the timeline, etc.
+
+Keep in mind that anonymous (not logged in) users can by default access most but not all of the features. You will need to configure authentication and grant additional [wiki:TracPermissions permissions] to authenticated users to see the full set of features.
+
+''Enjoy!''
+
+[http://projects.edgewall.com/trac/wiki/TracTeam The Trac Team]
+
+----
+See also:  TracGuide, TracCgi, TracFastCgi, TracModPython, TracUpgrade, TracPermissions
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracInterfaceCustomization
@@ -0,0 +1,68 @@
+= Customizing the Trac Interface =
+[[TracGuideToc]]
+
+== Introduction ==
+This page is meant to give users suggestions on how they can customize the look of Trac.  Topics on this page cover editing the HTML templates and CSS files, but not the program code itself.  The topics are intended to show users how they can modify the look of Trac to meet their specific needs.  Suggestions for changes to Trac's interface applicable to all users should be filed as tickets, not listed on this page.
+
+== Project Logo and Icon ==
+The easiest parts of the Trac interface to customize are the logo and the site icon.  Both of these can be configured with settings in [wiki:TracIni trac.ini].
+
+The logo or icon image should be put in a folder named "htdocs" in your project's environment folder.  (''Note: in projects created with a Trac version prior to 0.9 you will need to create this folder'')
+
+Now configure the appropriate section of your [wiki:TracIni trac.ini]:
+
+=== Logo ===
+Change the `src` setting to `site/` followed by the name of your image file.  The `width` and `height` settings should be modified to match your image's dimensions.
+
+{{{
+[header_logo]
+src = site/my_logo.gif
+alt = My Project
+width = 300
+height = 100
+}}}
+
+=== Icon ===
+Icons should be a 16x16 image in `.gif` or `.ico` format.  Change the `icon` setting to `site/` followed by the name of your icon file.  Icons will typically be displayed by your web browser next to the site's URL and in the `Bookmarks` menu.
+
+{{{
+[project]
+icon = site/my_icon.ico
+}}}
+
+Note though that this icon is ignored by Internet Explorer, which only accepts a file named ``favicon.ico`` at the root of the host. To make the project icon work in both IE and other browsers, you can store the icon in the document root of the host, and reference it from ``trac.ini`` as follows:
+
+{{{
+[project]
+icon = /favicon.ico
+}}}
+
+== Site Header & Footer ==
+In the environment folder for each Trac project there should be a directory called {{{templates}}}.  This folder contains files {{{site_header.cs}}} and {{{site_footer.cs}}}.  Users can customize their Trac site by adding the required HTML markup to these files.  The content of these two files will be placed immediately following the opening {{{<body>}}} tag and immediately preceding the closing {{{</body>}}} tag of each page in the site, respectively.
+
+These files may contain static HTML, though if users desire to have dynamically generated content they can make use of the [http://www.clearsilver.net/ ClearSilver] templating language from within the pages as well. When you need to see what variables are available to the template, append the query string `?hdfdump=1` to the URL of your Trac site. This will display a structured view of the template data.
+
+== Site CSS ==
+The primary means to adjust the layout of a Trac site is by add [http://www.w3.org/TR/REC-CSS2/ CSS] style rules that overlay the default rules. This is best done by editing the `site_css.cs` file in the enviroment's `templates` directory. The content of that template gets inserted into a `<style type="text/css></style>` element on every HTML page generated by Trac.
+
+While you can add your custom style rules directory to the `site_css.cs` file, it is recommended that you simply reference an external style sheet, thereby enabling browsers to cache the CSS file instead of transmitting the rules with every response.
+
+The following example would import a style sheet located in the `style` root directory of your host:
+{{{
+@import url(/style/mytrac.css);
+}}}
+
+You can use a !ClearSilver variable to reference a style sheet stored in the project environment's `htdocs` directory:
+{{{
+@import url(<?cs var:chrome.href ?>/site/style.css);
+}}}
+
+== Main Templates ==
+
+It is also possible to use your own modified versions of the Trac [http://www.clearsilver.net/ ClearSilver] templates. Note though that this technique is not recommended because it makes upgrading Trac rather problematic: there are unfortunately several dependencies between the templates and the application code, such as the name of form fields and the structure of the template data, and these are likely to change between different versions of Trac.
+
+If you absolutely need to use modified templates, copy the template files from the default templates directory (usually in found in `$prefix/share/trac/templates`) into the `templates` directory of the project environment. Then modify those copies to get the desired results.
+
+
+----
+See also TracGuide, TracIni
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracLinks
@@ -0,0 +1,101 @@
+= Trac Links =
+As you might have guessed, TracLinks are a very fundamental feature of Trac.
+
+They allow hyperlinking between Trac entities (tickets, reports, changesets, Wiki
+pages, milestones and source files) from anywhere WikiFormatting is used.
+
+TracLinks are generally of the form '''type:id''' (where ''id'' represents the
+number, name or path of the item) though some frequently used kinds of items
+also have short-hand notations.
+
+Some examples:
+ * Tickets: '''!#1''' or '''!ticket:1'''
+ * Reports: '''!{1}''' or '''!report:1'''
+ * Changesets: '''!r1''', '''![1]''' or '''!changeset:1'''
+ * Revision log: '''!r1:3''', '''![1:3]''' or '''!log:#1:3'''
+ * Wiki pages: '''CamelCase''' or '''!wiki:CamelCase'''
+ * Milestones: '''!milestone:1.0'''
+ * Attachment: '''!attachment:ticket:944:attachment.1073.diff'''
+ * Files: '''!source:trunk/COPYING'''
+ * A specific file revision: '''!source:/trunk/COPYING#200'''
+Display:
+ * Tickets: #1 or ticket:1
+ * Reports: {1} or report:1
+ * Changesets: r1, [1] or changeset:1
+ * Differences: r1:3, [1:3] or log:#1:3
+ * Wiki pages: CamelCase or wiki:CamelCase
+ * Milestones: milestone:1.0
+ * Files: source:trunk/COPYING
+ * Attachment: attachment:ticket:944:attachment.1073.diff
+ * A specific file revision: source:/trunk/COPYING#200
+
+'''Note:''' The wiki:CamelCase form is rarely used, but it can be convenient to refer to
+pages whose names do not follow WikiPageNames rules, i.e., single words,
+non-alphabetic characters, etc.
+
+Trac links using the full (non-shorthand) notation can also be given a custom
+link title like this:
+
+{{{
+[ticket:1 This is a link to ticket number one].
+}}}
+
+Display: [ticket:1 This is a link to ticket number one].
+
+If the title is be omitted, only the id (the part after the colon) is displayed:
+
+{{{
+[ticket:1]
+}}}
+
+Display: [ticket:1]
+
+It might seem a simple enough concept at a glance, but actually allows quite a complex network of information. In practice, it's very intuitive and simple to use, and we've found the "link trail" extremely helpful to better understand what's happening in a project or why a particular change was made.
+
+== attachement: links ==
+
+The link syntax for attachments is as follows:
+ * !attachment:the_file.txt creates a link to the attachment the_file.txt of the current object
+ * !attachment:wiki:MyPage:the_file.txt creates a link to the attachment the_file.txt of the !MyPage wiki page
+ * !attachment:ticket:753:the_file.txt creates a link to the attachment the_file.txt of the ticket 753 !attachment:wiki:MyPage:the_file.txt
+
+== source: links ==
+
+The default behavior for a source:/some/path link is to open the directory browser 
+if the path points to a directory and otherwise open the log view. 
+It's also possible to link directly to a specific revision of a file like this: source:/some/file@123 
+or like this to link to the latest revision: source:/some/file@latest.
+If the revision is specified, one can even link to a specific line number: !source:/some/file@123#L10 
+[[comment(TODO: remove the ! when Edgewall Trac is upgraded with the support for the line syntax)]]
+
+== Quoting space in TracLinks ==
+
+The usual syntax for quoting space is:
+
+ * !attachment:'the file.txt' or
+ * !attachment:"the file.txt" 
+
+== Where to use TracLinks ==
+You can use TracLinks in:
+
+ * Source code (Subversion) commit messages
+ * Wiki pages
+ * Full descriptions for tickets, reports and milestones
+
+and any other text fields explicitly marked as supporting WikiFormatting.
+
+== Escaping Links ==
+
+To prevent parsing of a !TracLink, you can escape it by preceding it with a '!' (exclamation mark).
+{{{
+ !NoLinkHere.
+ ![42] is not a link either.
+}}}
+
+Display:
+ !NoLinkHere.
+ ![42] is not a link either.
+
+----
+See also: WikiFormatting, TracWiki
+ 
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracLogging
@@ -0,0 +1,29 @@
+= Trac Logging =
+[[TracGuideToc]]
+
+Trac supports logging of system messages using the standard [http://docs.python.org/lib/module-logging.html logging module] that comes with Python.
+
+Logging is configured in the {{{[logging]}}} section in [wiki:TracIni trac.ini].
+
+== Supported Logging Methods ==
+
+The log method is set using the `log_type` configuration option, which takes any of the following values:
+ '''none'':: Suppress all log messages.
+ '''file''':: Log messages to a file, specified with the `log_file` option in [wiki:TracIni trac.ini]. 
+ '''stderr''':: Output all log entries to console ([wiki:TracStandalone tracd] only).
+ '''syslog''':: (UNIX) Send messages to local syslogd via named pipe `/dev/log`.
+ '''eventlog''':: (Windows) Use the system's NT eventlog for Trac logging.
+
+== Log Levels ==
+
+The verbosity level of logged messages can be set using the ''log_level'' directive in [wiki:TracIni trac.ini]. The log level defines the minimum level of urgency required for a message to be logged.
+
+The levels are:
+ '''CRITICAL''':: Log only the most critical (typically fatal) errors.
+ '''ERROR''':: Log failures, bugs and errors. 
+ '''WARN''':: Log warnings, non-interrupting events.
+ '''INFO''':: Diagnostic information, log information about all processing.
+ '''DEBUG''':: Trace messages, profiling, etc.
+
+----
+See also: TracIni, TracGuide, TracEnvironment
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracModPython
@@ -0,0 +1,129 @@
+= Trac and mod_python =
+
+Trac supports [http://www.modpython.org/ mod_python], which speeds up Trac's response times considerably and permits use of many Apache features not possible with [wiki:TracStandalone tracd]/mod_proxy.
+
+== Simple configuration ==
+
+If you just installed mod_python, you may have to add a line to load the module in the Apache configuration:
+{{{
+LoadModule python_module modules/mod_python.so
+}}}
+
+A simple setup of Trac on mod_python looks like this:
+{{{
+<Location /projects/myproject>
+   SetHandler mod_python
+   PythonHandler trac.web.modpython_frontend 
+   PythonOption TracEnv /var/trac/myproject
+   PythonOption TracUriRoot /projects/myproject
+</Location>
+}}}
+
+Note that the option `TracUriRoot` may or may not be necessary in your setup. Try without first, and if the URLs produced by Trac look wrong or if Trac does not seem to recognize the URLs correctly, add the `TracUriRoot` option.
+
+Configuring authentication works the same as for [wiki:TracCgi#AddingAuthentication CGI]:
+{{{
+<Location "/projects/myproject/login">
+  AuthType Basic
+  AuthName "myproject"
+  AuthUserFile /var/trac/myproject/.htaccess
+  Require valid-user
+</Location>
+}}}
+
+If the Trac installation isn't installed in your Python path, you'll have to tell Apache where to find the Trac mod_python handler  using the `PythonPath` directive:
+{{{
+<Location /projects/myproject>
+  ...
+  PythonPath "sys.path + ['/path/to/trac']"
+  ...
+</Location>
+}}}
+
+
+== Setting up multiple projects ==
+
+The Trac mod_python handler handler supports a configuration option similar to Subversion's `SvnParentPath`, called `TracEnvParentDir`:
+{{{
+<Location /projects>
+  SetHandler mod_python
+  PythonHandler trac.web.modpython_frontend 
+  PythonOption TracEnvParentDir /var/trac
+  PythonOption TracUriRoot /projects
+</Location>
+}}}
+
+When you request the `/projects` URL, you will get a listing of all subdirectories of the directory you set as `TracEnvParentDir`. Selecting any project in the list will bring you to the corresponding Trac environment.
+
+If you don't want to have the subdirectory listing as your projects home page you can use a
+{{{
+<LocationMatch "/.+/">
+}}}
+
+This will instruct Apache to use mod_python for all locations different from root while having the possibility of placing a custom home page for root in your !DocumentRoot folder.
+
+You can also use the same authentication realm for all of the projects using a `<LocationMatch>` directive:
+{{{
+<LocationMatch "/[^/]+/login">
+  AuthType Basic
+  AuthName "Trac"
+  AuthUserFile /var/trac/.htaccess
+  Require valid-user
+</LocationMatch>
+}}}
+
+== Virtual Host Configuration ==
+
+Below is the sample configuration required to set up your trac as a virtual server (i.e. when you access it at the URLs like
+!http://trac.mycompany.com):
+
+{{{
+<VirtualHost * >
+    DocumentRoot /var/trac/myproject
+    ServerName trac.mycompany.com
+    <Directory />
+        SetHandler mod_python
+        PythonHandler trac.web.modpython_frontend
+        PythonOption TracEnv /var/trac/myproject
+        PythonOption TracUriRoot /
+    </Directory>
+    <Location /login>
+        AuthType Basic
+        AuthName "MyCompany Trac Server"
+        AuthUserFile /var/trac/myproject/.htusers
+        Require valid-user
+    </Location>
+</VirtualHost>
+}}}
+
+== Troubleshooting ==
+
+=== Form submission problems ===
+
+If you're experiencing problems submitting some of the forms in Trac (a common problem is that you get redirected to the start page after submission), check whether your {{{DocumentRoot}}} contains a folder or file with the same path that you mapped the mod_python handler to. For some reason, mod_python gets confused when it is mapped to a location that also matches a static resource.
+
+=== Using .htaccess ===
+
+Although it may seem trivial to rewrite the above configuration as a directory in your document root with a `.htaccess` file, this does not work. Apache will append a "/" to any Trac URLs, which interferes with its correct operation.
+
+It may be possible to work around this with mod_rewrite, but I failed to get this working. In all, it is more hassle than it is worth. Stick to the provided instructions. :)
+
+=== Win32 Issues ===
+
+If you run trac with mod_python (3.1.3 or 3.1.4) on Windows, 
+uploading attachments will '''not''' work.
+This is a known problem which we can't solve cleanly at the Trac level.
+
+However, there is a workaround for this at the mod_python level, 
+which is to apply the following patch [http://projects.edgewall.com/trac/attachment/ticket/554/util_py.patch attachment:ticket:554:util_py.patch] 
+to the (Lib/site-packages)/modpython/util.py file.
+
+If you don't have the `patch` command, that file can be replaced with the [http://svn.apache.org/viewcvs.cgi/httpd/mod_python/trunk/lib/python/mod_python/util.py?rev=103562&view=markup  fixed util.py] (fix which, although done prior to the 3.1.4 release, is ''not'' 
+present in 3.1.4).
+
+=== OS X issues ===
+
+When using mod_python on OS X you will not be able to restart Apache using `apachectl restart`. This is apparently fixed in mod_python 3.2, but there's also a patch available for earlier versions [http://www.dscpl.com.au/projects/vampire/patches.html here].
+
+----
+See also TracGuide, TracInstall, TracCgi, TracFastCgi
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracNotification
@@ -0,0 +1,68 @@
+= Email Notification of Ticket Changes =
+[[TracGuideToc]]
+
+Trac supports basic notification for ticket changes using email. 
+
+Email notification is useful to keep users up-to-date on tickets/issues of interest, and also provides a convenient way to post ticket changes to a dedicated mailing list. '''Note:''' As an example, this is how the [http://lists.edgewall.com/archive/trac-tickets/ Trac-tickets] mailing list works.
+
+Disabled by default, notification can be activated and configured in [wiki:TracIni trac.ini].
+
+== Receiving Notification ==
+When reporting a new ticket or adding a comment, enter a valid email address in the ''reporter'', ''editor'' or ''cc'' field. Trac will automatically send you an email when changes are made to the ticket.
+
+This is useful to keep up-to-date on an issue or enhancement request that interests you.
+
+== Configuring SMTP Notification ==
+
+=== Configuration Options ===
+These are the available options for the ''[notification]'' section in trac.ini.
+ * '''smtp_enabled''': Enable email notification.
+ * '''smtp_server''': SMTP server used for notification messages.
+ * '''smtp_user''': (''requires [milestone:0.9 0.9]'') user name for authentication SMTP account.
+ * '''smtp_password''': (''requires [milestone:0.9 0.9]'') password for authentication SMTP account.
+ * '''smtp_from''': Email address to use for ''Sender''-headers in notification emails.
+ * '''smtp_replyto''': Email address to use for ''Reply-To''-headers in notification emails.
+ * '''smtp_always_cc''': List of email addresses to always send notifications to. ''Typically used to post ticket changes to a dedicated mailing list.''
+ * '''always_notify_reporter''':  Always send notifications to any address in the reporter field.
+ * '''always_notify_owner''': (''requires [milestone:0.9 0.9]'') Always send notifications to the address in the owner field.
+
+Either '''smtp_from''' or '''smtp_replyto''' (or both) ''must'' be set, otherwise Trac refuses to send notification mails.
+
+=== Example Configuration ===
+
+{{{
+[notification]
+smtp_enabled = true
+smtp_server = mail.example.com
+smtp_from = notifier@example.com
+smtp_replyto = myproj@projects.example.com
+smtp_always_cc = ticketmaster@example.com, theboss+myproj@example.com
+}}}
+
+== Sample Email ==
+{{{
+#42: testing
+---------------------------+------------------------------------------------
+       Id:  42             |      Status:  assigned                
+Component:  report system  |    Modified:  Fri Apr  9 00:04:31 2004
+ Severity:  major          |   Milestone:  0.9                     
+ Priority:  lowest         |     Version:  0.6                     
+    Owner:  anonymous      |    Reporter:  jonas@example.com               
+---------------------------+------------------------------------------------
+Changes:
+  * component:  changset view => search system
+  * priority:  low => highest
+  * owner:  jonas => anonymous
+  * cc:  daniel@example.com =>
+         daniel@example.com, jonas@example.com
+  * status:  new => assigned
+
+Comment:
+I'm interested too!
+
+--
+Ticket URL: <http://example.com/trac/ticket/42>
+My Project <http://myproj.example.com/>
+}}}
+----
+See also: TracTickets, TracIni, TracGuide
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracPermissions
@@ -0,0 +1,100 @@
+= Trac Permissions =
+[[TracGuideToc]]
+
+Trac uses a simple but flexible permission system to control what users can and can't access.
+
+Permission privileges are managed using the [wiki:TracAdmin trac-admin] tool.
+
+Regular visitors, non-authenticated users, accessing the system are assigned the default 
+role (''user'') named {{{anonymous}}}. 
+Assign permissions to the {{{anonymous}}} user to set privileges for non-authenticated/guest users.
+
+In addition to these privileges users can be granted additional individual 
+rights in effect when authenticated and logged into the system.
+
+== Available Privileges ==
+
+To enable all privileges for a user, use the `TRAC_ADMIN` permission. Having `TRAC_ADMIN` is like being `root` on a *NIX system, it will let you do anything you want.
+
+Otherwise, individual privileges can be assigned to users for the various different functional areas of Trac:
+
+=== Repository Browser ===
+
+|| `BROWSER_VIEW` || View directory listings in the [wiki:TracBrowser repository browser] ||
+|| `LOG_VIEW` || View revision logs of files and directories in the [wiki:TracBrowser repository browser] ||
+|| `FILE_VIEW` || View files in the [wiki:TracBrowser repository browser] ||
+|| `CHANGESET_VIEW` || View [wiki:TracChangeset repository check-ins] ||
+
+=== Ticket System ===
+
+|| `TICKET_VIEW` || View existing [wiki:TracTickets tickets] and perform [wiki:TracQuery ticket queries] ||
+|| `TICKET_CREATE` || Create new [wiki:TracTickets tickets] ||
+|| `TICKET_APPEND` || Add comments or attachments to [wiki:TracTickets tickets] ||
+|| `TICKET_CHGPROP` || Modify [wiki:TracTickets ticket] properties ||
+|| `TICKET_MODIFY` || Includes both `TICKET_APPEND` and `TICKET_CHGPROP`, and in addition allows resolving [wiki:TracTickets tickets] ||
+|| `TICKET_ADMIN` || All `TICKET_*` permissions, plus the deletion of ticket attachments. ||
+
+=== Roadmap ===
+
+|| `MILESTONE_VIEW` || View a milestone ||
+|| `MILESTONE_CREATE` || Create a new milestone ||
+|| `MILESTONE_MODIFY` || Modify existing milestones ||
+|| `MILESTONE_DELETE` || Delete milestones ||
+|| `MILESTONE_ADMIN` || All `MILESTONE_*` permissions ||
+|| `ROADMAP_VIEW` || View the [wiki:TracRoadmap roadmap] page ||
+|| `ROADMAP_ADMIN` || Alias for `MILESTONE_ADMIN` (deprecated) ||
+
+=== Reports ===
+
+|| `REPORT_VIEW` || View [wiki:TracReports reports] ||
+|| `REPORT_SQL_VIEW` || View the underlying SQL query of a [wiki:TracReports report] ||
+|| `REPORT_CREATE` || Create new [wiki:TracReports reports] ||
+|| `REPORT_MODIFY` || Modify existing [wiki:TracReports reports] ||
+|| `REPORT_DELETE` || Delete [wiki:TracReports reports] ||
+|| `REPORT_ADMIN` || All `REPORT_*` permissions ||
+
+=== Wiki System ===
+
+|| `WIKI_VIEW` || View existing [wiki:TracWiki wiki] pages ||
+|| `WIKI_CREATE` || Create new [wiki:TracWiki wiki] pages ||
+|| `WIKI_MODIFY` || Change [wiki:TracWiki wiki] pages ||
+|| `WIKI_DELETE` || Delete [wiki:TracWiki wiki] pages and attachments ||
+|| `WIKI_ADMIN` || All `WIKI_*` permissions, plus the management of ''readonly'' pages. ||
+
+=== Others ===
+
+|| `TIMELINE_VIEW` || View the [wiki:TracTimeline timeline] page ||
+|| `SEARCH_VIEW` || View and execute [wiki:TracSearch search] queries ||
+|| `CONFIG_VIEW` || Enables additional pages on ''About Trac'' that show the current configuration or the list of installed plugins ||
+
+== Granting Privileges ==
+
+Currently the only way to grant privileges to users is by using the `trac-admin` script. The current set of privileges can be listed with the following command:
+{{{
+  $ trac-admin /path/to/projenv permission list
+}}}
+
+This command will allow the user ''bob'' to delete reports:
+{{{
+  $ trac-admin /path/to/projenv permission add bob REPORT_DELETE
+}}}
+
+== Permission Groups ==
+
+Permissions can be grouped together to form roles such as ''developer'', ''admin'', etc.
+{{{
+  $ trac-admin /path/to/projenv permission add developer WIKI_ADMIN
+  $ trac-admin /path/to/projenv permission add developer REPORT_ADMIN
+  $ trac-admin /path/to/projenv permission add developer TICKET_MODIFY
+  $ trac-admin /path/to/projenv permission add bob developer
+  $ trac-admin /path/to/projenv permission add john developer
+}}}
+
+== Default Permissions ==
+
+Granting privileges to the special user ''anonymous'' can be used to control what an anonymous user can do before they have logged in.
+
+In the same way, privileges granted to the special user ''authenticated'' will apply to any authenticated (logged in) user.
+
+----
+See also: TracAdmin, TracGuide
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracPlugins
@@ -0,0 +1,77 @@
+= Trac Plugins =
+[[TracGuideToc]]
+
+Since version 0.9, Trac supports plugins that extend the built-in functionality. The plugin functionality is based on the [http://projects.edgewall.com/trac/wiki/TracDev/ComponentArchitecture component architecture].
+
+== Requirements ==
+
+To use plugins in Trac, you need to have [http://peak.telecommunity.com/DevCenter/setuptools setuptools] (version 0.6) installed.
+
+To install `setuptools`, download the bootstrap module [http://peak.telecommunity.com/dist/ez_setup.py ez_setup.py] and execute it as follows:
+{{{
+$ python ez_setup.py
+}}}
+
+If the `ez_setup.py` script fails to install the setuptools release, you can download it from [http://www.python.org/pypi/setuptools PyPI] and install it manually.
+
+== Installing a Trac Plugin ==
+
+=== For a Single Project ===
+
+Plugins are packaged as [http://peak.telecommunity.com/DevCenter/PythonEggs Python eggs]. That means they are ZIP archives with the file extension `.egg`. If you have downloaded a source distribution of a plugin, you can run:
+{{{
+$ python setup.py bdist_egg
+}}}
+to build the `.egg` file.
+
+Once you have the plugin archive, you need to copy it into the `plugins` directory of the [wiki:TracEnvironment project environment]. Also, make sure that the web server has sufficient permissions to read the plugin egg.
+
+=== For All Projects ===
+
+Plugins that you want to use in all your projects (such as [http://projects.edgewall.com/trac/wiki/WebAdmin WebAdmin]) can be installed globally by running:
+{{{
+$ python setup.py install
+}}}
+
+Alternatively, you can just drop the `.egg` file in the Python `site-packages` directory.
+
+Unlike plugins installed per-environment, you'll have to explicitly enable globally installed plugins via [wiki:TracIni trac.ini]. This is done in the `[components]` section of the configuration file, for example:
+{{{
+[components]
+webadmin.* = enabled
+}}}
+
+The name of the option is the Python package of the plugin. This should be specified in the documentation of the Plugin, but can also be easily find out by looking at the source (look for a top-level directory that contains a file named `__init__.py`.)
+
+== Setting up the Plugin Cache ==
+
+Some plugins will need to be extracted by the Python eggs runtime (`pkg_resources`), so that their contents are actual files on the file system. The directory in which they are extracted defaults to the home directory of the current user, which may or may not be a problem. You can however override the default location using the `PYTHON_EGG_CACHE` environment variable.
+
+To do this from the Apache configuration, use the `SetEnv` directive as follows:
+{{{
+SetEnv PYTHON_EGG_CACHE /path/to/dir
+}}}
+
+This works whether your using the [wiki:TracCgi CGI] or the [wiki:TracModPython mod_python] front-end. Put this directive next to where you set the path to the [wiki:TracEnvironment Trac environment], i.e. in the same `<Location>` block.
+
+For example (for CGI):
+{{{
+ <Location /trac>
+   SetEnv TRAC_ENV /path/to/projenv
+   SetEnv PYTHON_EGG_CACHE /path/to/dir
+ </Location>
+}}}
+
+or (for mod_python):
+{{{
+ <Location /trac>
+   SetHandler mod_python
+   ...
+   SetEnv PYTHON_EGG_CACHE /path/to/dir
+ </Location>
+}}}
+
+For [wiki:TracFastCgi FastCGI], you'll need to `-initial-env` option, or whatever is provided by your web server for setting environment variables.
+
+----
+See also TracGuide, [http://projects.edgewall.com/trac/wiki/PluginList plugin list], [http://projects.edgewall.com/trac/wiki/TracDev/ComponentArchitecture component architecture]
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracQuery
@@ -0,0 +1,85 @@
+= Trac Ticket Queries =
+[[TracGuideToc]]
+
+In addition to [wiki:TracReports reports], Trac provides support for ''custom ticket queries'', used to display lists of tickets meeting a specified set of criteria. 
+
+To configure and execute a custom query, switch to the ''View Tickets'' module from the navigation bar, and select the ''Custom Query'' link.
+
+== Filters ==
+When you first go to the query page the default filters will display all open tickets, or if you're logged in it will display open tickets assigned to you.  Current filters can be removed by clicking the button to the right with the minus sign on the label.  New filters are added from the pulldown list in the bottom-right corner of the filters box.  Filters with either a text box or a pulldown menu of options can be added multiple times to perform an ''or'' of the criteria.
+
+You can use the fields just below the filters box to group the results based on a field, or display the full description for each ticket.
+
+Once you've edited your filters click the ''Update'' button to refresh your results.
+
+== Navigating Tickets ==
+Clicking on one of the query results will take you to that ticket.  You can navigate through the results by clicking the ''Next Ticket'' or ''Previous Ticket'' links just below the main menu bar, or click the ''Back to Query'' link to return to the query page.  
+
+You can safely edit any of the tickets and continue to navigate through the results using the ''Next/Previous/Back to Query'' links after saving your results.  When you return to the query any tickets you edited will be displayed with italicized text.  If one of the tickets was edited such that it no longer matches the query criteria the text will also be greyed.  The query results can be refreshed and cleared of these status indicators by clicking the ''Update'' button again.
+
+== Saving Queries ==
+
+While Trac does not yet allow saving a named query and somehow making it available in a navigable list, you can save references to queries in Wiki content, as described below.
+
+=== Using TracLinks ===
+
+You may want to save some queries so that you can come back to them later.  You can do this by making a link to the query from any Wiki page.
+{{{
+[query:status=new|assigned|reopened&version=0.8 Active tickets against 0.8]
+}}}
+
+Which is displayed as:
+  [query:status=new|assigned|reopened&version=0.8 Active tickets against 0.8]
+
+This uses a very simple query language to specify the criteria (see [wiki:TracQuery#QueryLanguage Query Language]).
+
+Alternatively, you can copy the query string of a query and paste that into the Wiki link, including the leading `?` character:
+{{{
+[query:?status=new&status=assigned&status=reopened&group=owner Assigned tickets by owner]
+}}}
+
+Whis is displayed as:
+  [query:?status=new&status=assigned&status=reopened&group=owner Assigned tickets by owner]
+
+The advantage of this approach is that you can also specify the grouping and ordering, which is not possible using the first syntax.
+
+=== Using the `[[TicketQuery]]` Macro ===
+
+The `[[TicketQuery]]` macro lets you display lists of tickets matching certain criteria anywhere you can use WikiFormatting.
+
+Example:
+{{{
+[[TicketQuery(version=0.9b1|0.9b2&resolution=duplicate)]]
+}}}
+
+This is displayed as:
+  [[TicketQuery(version=0.9b1|0.9b2&resolution=duplicate)]]
+
+Just like the [wiki:TracQuery#UsingTracLinks query: wiki links], the parameter of this macro expects a query string formatted according to the rules of the simple [wiki:TracQuery#QueryLanguage ticket query language].
+
+A more compact representation without the ticket summaries is also available:
+{{{
+[[TicketQuery(version=0.9b1|0.9b2&resolution=duplicate, compact)]]
+}}}
+
+This is displayed as:
+  [[TicketQuery(version=0.9b1|0.9b2&resolution=duplicate, compact)]]
+
+=== Query Language ===
+
+`query:` TracLinks and the `[[TicketQuery]]` macro both use a mini “query language†for specifying query filters. Basically, the filters are separate by ampersands (`&`). Each filter then consists of the ticket field name, an operator, and one or more values. More than one value are separated by a pipe (`|`), meaning that the filter matches any of the values.
+
+The available operators are:
+|| '''=''' || the field content exactly matches the one of the values ||
+|| '''~=''' || the field content contains one or more of the values ||
+|| '''!^=''' || the field content starts with one of the values ||
+|| '''$=''' || the field content ends with one of the values ||
+
+All of these operators can also be negated:
+|| '''!=''' || the field content matches none of the values ||
+|| '''!~=''' || the field content does not contain any of the values ||
+|| '''!!^=''' || the field content does not start with any of the values ||
+|| '''!$=''' || the field content does not end with any of the values ||
+
+----
+See also: TracTickets, TracReports, TracGuide
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracReports
@@ -0,0 +1,216 @@
+= Trac Reports =
+[[TracGuideToc]]
+
+The Trac reports module provides a simple, yet powerful reporting facility
+to present information about tickets in the Trac database.
+
+Rather than have its own report definition format, TracReports relies on standard SQL
+`SELECT` statements for custom report definition. 
+
+  '''Note:''' ''The report module is being phased out in its current form because it seriously limits the ability of the Trac team to make adjustments to the underlying database schema. We believe that the [wiki:TracQuery query module] is a good replacement that provides more flexibility and better usability. While there are certain reports that cannot yet be handled by the query module, we intend to further enhance it so that at some point the reports module can be completely removed. This also means that there will be no major enhancements to the report module anymore.''
+
+  ''You can already completely replace the reports module by the query module simply be disabling the former in [wiki:TracIni trac.ini]:''
+  {{{
+  [components]
+  trac.ticket.report.* = disabled
+  }}}
+  ''This will make the query module the default handler for the “View Tickets†navigation item. We encourage you to try this configuration and report back what kind of features of reports you are missing, if any.''
+
+A report consists of these basic parts:
+ * '''ID''' -- Unique (sequential) identifier 
+ * '''Title'''  -- Descriptive title
+ * '''Description'''  -- A brief description of the report, in WikiFormatting text.
+ * '''Report Body''' -- List of results from report query, formatted according to the methods described below.
+ * '''Footer''' -- Links to alternative download formats for this report.
+
+== Changing Sort Order ==
+Simple reports - ungrouped reports to be specific - can be changed to be sorted by any column simply by clicking the column header. 
+
+If a column header is a hyperlink (red), click the column you would like to sort by. Clicking the same header again reverses the order.
+
+
+== Alternate Download Formats ==
+Aside from the default HTML view, reports can also be exported in a number of alternate formats.
+At the bottom of the report page, you will find a list of available data formats. Click the desired link to 
+download the alternate report format.
+
+=== Comma-delimited - CSV (Comma Separated Values) ===
+Export the report as plain text, each row on its own line, columns separated by a single comma (',').
+'''Note:''' Carriage returns, line feeds, and commas are stripped from column data to preserve the CSV structure.
+
+=== Tab-delimited ===
+Like above, but uses tabs (\t) instead of comma.
+
+=== RSS - XML Content Syndication ===
+All reports support syndication using XML/RSS 2.0. To subscribe to an RSS feed, click the orange 'XML' icon at the bottom of the page. See TracRss for general information on RSS support in Trac.
+
+----
+
+== Creating Custom Reports ==
+
+''Creating a custom report requires a comfortable knowledge of SQL.''
+
+A report is basically a single named SQL query, executed and presented by
+Trac.  Reports can be viewed and created from a custom SQL expression directly
+in from the web interface.
+
+Typically, a report consists of a SELECT-expression from the 'ticket' table,
+using the available columns and sorting the way you want it.
+
+== Ticket columns ==
+The ''ticket'' table has the following columns:
+ * id
+ * time
+ * changetime
+ * component
+ * severity  
+ * priority 
+ * owner
+ * reporter
+ * cc
+ * version
+ * milestone
+ * status
+ * resolution
+ * summary
+ * description
+
+See TracTickets for a detailed description of the column fields.
+
+'''all active tickets, sorted by priority and time'''
+
+'''Example:''' ''All active tickets, sorted by priority and time''
+{{{
+SELECT id AS ticket, status, severity, priority, owner, 
+       time as created, summary FROM ticket 
+  WHERE status IN ('new', 'assigned', 'reopened')
+  ORDER BY priority, time
+}}}
+
+
+----
+
+
+== Advanced Reports: Dynamic Variables ==
+For more flexible reports, Trac supports the use of ''dynamic variables'' in report SQL statements. 
+In short, dynamic variables are ''special'' strings that are replaced by custom data before query execution.
+
+=== Using Variables in a Query ===
+The syntax for dynamic variables is simple, any upper case word beginning with '$' is considered a variable.
+
+Example:
+{{{
+SELECT id AS ticket,summary FROM ticket WHERE priority='$PRIORITY'
+}}}
+
+To assign a value to $PRIORITY when viewing the report, you must define it as an argument in the report URL, leaving out the the leading '$'.
+
+Example:
+{{{
+ http://projects.edgewall.com/trac/reports/14?PRIORITY=high
+}}}
+
+To use multiple variables, separate them with an '&'.
+
+Example:
+{{{
+ http://projects.edgewall.com/trac/reports/14?PRIORITY=high&SEVERITY=critical
+}}}
+
+
+=== Special/Constant Variables ===
+There is one ''magic'' dynamic variable to allow practical reports, its value automatically set without having to change the URL. 
+
+ * $USER -- Username of logged in user.
+
+Example (''List all tickets assigned to me''):
+{{{
+SELECT id AS ticket,summary FROM ticket WHERE owner='$USER'
+}}}
+
+
+----
+
+
+== Advanced Reports: Custom Formatting ==
+Trac is also capable of more advanced reports, including custom layouts,
+result grouping and user-defined CSS styles. To create such reports, we'll use
+specialized SQL statements to control the output of the Trac report engine.
+
+== Special Columns ==
+To format reports, TracReports looks for 'magic' column names in the query
+result. These 'magic' names are processed and affect the layout and style of the 
+final report.
+
+=== Automatically formatted columns ===
+ * '''ticket''' -- Ticket ID number. Becomes a hyperlink to that ticket. 
+ * '''created, modified, date, time''' -- Format cell as a date and/or time.
+
+ * '''description''' -- Ticket description field, parsed through the wiki engine.
+
+'''Example:'''
+{{{
+SELECT id as ticket, created, status, summary FROM ticket 
+}}}
+
+=== Custom formatting columns ===
+Columns whose names begin and end with 2 underscores (Example: '''_''''''_color_''''''_''') are
+assumed to be ''formatting hints'', affecting the appearance of the row.
+ 
+ * '''_''''''_group_''''''_''' -- Group results based on values in this column. Each group will have its own header and table.
+ * '''_''''''_color_''''''_''' -- Should be a numeric value ranging from 1 to 5 to select a pre-defined row color. Typically used to color rows by issue priority.
+ * '''_''''''_style_''''''_''' -- A custom CSS style expression to use for the current row. 
+
+'''Example:''' ''List active tickets, grouped by milestone, colored by priority''
+{{{
+SELECT p.value AS __color__,
+     t.milestone AS __group__,
+     (CASE owner WHEN 'daniel' THEN 'font-weight: bold; background: red;' ELSE '' END) AS __style__,
+       t.id AS ticket, summary
+  FROM ticket t,enum p
+  WHERE t.status IN ('new', 'assigned', 'reopened') 
+    AND p.name=t.priority AND p.type='priority'
+  ORDER BY t.milestone, p.value, t.severity, t.time
+}}}
+
+'''Note:''' A table join is used to match ''ticket'' priorities with their
+numeric representation from the ''enum'' table.
+
+=== Changing layout of report rows ===
+By default, all columns on each row are display on a single row in the HTML
+report, possibly formatted according to the descriptions above. However, it's
+also possible to create multi-line report entries.
+
+ * '''column_''' -- ''Break row after this''. By appending an underscore ('_') to the column name, the remaining columns will be be continued on a second line.
+
+ * '''_column_''' -- ''Full row''. By adding an underscore ('_') both at the beginning and the end of a column name, the data will be shown on a separate row.
+
+ * '''_column'''  --  ''Hide data''. Prepending an underscore ('_') to a column name instructs Trac to hide the contents from the HTML output. This is useful for information to be visible only if downloaded in other formats (like CSV or RSS/XML).
+
+'''Example:''' ''List active tickets, grouped by milestone, colored by priority, with  description and multi-line layout''
+
+{{{
+SELECT p.value AS __color__,
+       t.milestone AS __group__,
+       (CASE owner 
+          WHEN 'daniel' THEN 'font-weight: bold; background: red;' 
+          ELSE '' END) AS __style__,
+       t.id AS ticket, summary AS summary_,             -- ## Break line here
+       component,version, severity, milestone, status, owner,
+       time AS created, changetime AS modified,         -- ## Dates are formatted
+       description AS _description_,                    -- ## Uses a full row
+       changetime AS _changetime, reporter AS _reporter -- ## Hidden from HTML output
+  FROM ticket t,enum p
+  WHERE t.status IN ('new', 'assigned', 'reopened') 
+    AND p.name=t.priority AND p.type='priority'
+  ORDER BY t.milestone, p.value, t.severity, t.time
+}}}
+
+=== Reporting on custom fields ===
+
+If you have added custom fields to your tickets (experimental feature in v0.8, see TracTicketsCustomFields), you can write a SQL query to cover them. You'll need to make a join on the ticket_custom table, but this isn't especially easy.
+
+If you have tickets in the database ''before'' you declare the extra fields in trac.ini, there will be no associated data in the ticket_custom table. To get around this, use SQL's "LEFT OUTER JOIN" clauses. See TracIniReportCustomFieldSample for some examples.
+
+----
+See also: TracTickets, TracQuery, TracGuide
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracRevisionLog
@@ -0,0 +1,102 @@
+= Viewing Revision Logs =
+[[TracGuideToc]]
+
+When you browse the repository, it's always possible to query the 
+''Revision Log'' view corresponding to the path you're currently seeing.
+This will display a list of the most recent changesets in which the 
+current path or any other path below it has been modified.
+
+== The Revision Log Form ==
+
+It's possible to set the revision at which the revision log should
+start, using the ''View log starting at'' field. An empty value
+or a value of ''head'' is taken to be the newest changeset. 
+
+It's also possible to specify the revision at which the log should
+stop, using the ''back to'' field. By default, it's left empty, 
+which means the revision log will stop as soon as 100 revisions have 
+been listed.
+
+Also, there are three modes of operation of the revision log.
+
+By default, the revision log ''stops on copy'', which means that 
+whenever an ''Add'', ''Copy'' or ''Rename'' operation is detected, 
+no older revision will be shown. That's very convenient when working
+with branches, as one only sees the history corresponding to what
+has been done on the branch.
+
+It's also possible to indicate that one wants to see what happened
+before a ''Copy'' or ''Rename'' change, by selecting the 
+''Follow copies'' mode. This will cross all copies or renames changes.
+Each time the name of the path changes, there will be an additional
+indentation level. That way, the changes on the different paths
+are easily grouped together visually.
+
+It's even possible to go past an ''Add'' change, in order to see 
+if there has been a ''Delete'' change on that path, before 
+that ''Add''. This mode corresponds to the mode called 
+''Show only adds, moves and deletes''. 
+While quite useful at times, be aware that this operation is quite 
+resource intensive.
+
+Finally, there's also a checkbox ''Show full log messages'',
+which controls whether the full content of the commit log message
+should be displayed for each change, or only a shortened version of it.
+
+== The Revision Log Information ==
+
+For each revision log entry, there are 7 columns shown:
+ 1. The first column contains a pair of radio buttons and should used 
+    for selecting the ''old'' and the ''new'' revisions that will be 
+    used for [wiki:TracRevisionLog#viewingtheactualchanges viewing the actual changes].
+ 2. A color code (similar to the one explained [wiki:TracChangeset here]) 
+    indicating kind of change.
+    Clicking on this column refreshes the revision log so that it restarts
+    with this change.
+ 3. The '''Date''' at which the change was made.
+ 4. The '''Revision''' number, displayed as `@xyz`. 
+    This is a link to the TracBrowser, using that revision as the base line.
+ 5. The '''Changeset''' number, displayed as `[xyz]`.
+    This is a link to the TracChangeset view.
+ 6. The '''Author''' of the change.
+ 7. The '''Log Message''', which contains either a summary or the full commit 
+    log message, depending on the value of the ''Show full log messages'' 
+    checkbox in the form above.
+    
+
+== Viewing the Actual Changes ==
+
+''(requires [milestone:0.10])''
+
+The ''View changes...'' buttons (placed above and below the list
+of changes, on the left side) will show the set of differences
+corresponding to the aggregated changes starting from the ''old''
+revision (first radio-button) to the ''new'' revision (second
+radio-button), in the TracChangeset view.
+
+Note that the ''old'' revision doesn't need to be actually 
+''older'' than the ''new'' revision: it simply gives a base
+for the diff. It's therefore entirely possible to easily 
+generate a ''reverse diff'', for reverting what has been done
+in the given range of revisions.
+
+Finally, if the two revisions are identical, the corresponding
+changeset will be shown (same effect as clicking on column 5).
+
+
+== Alternative Formats ==
+
+=== The ChangeLog Text ===
+
+At the bottom of the page, there's a ''ChangeLog'' link
+that will show the range of revisions as currently shown,
+but as a simple text, matching the usual conventions for
+!ChangeLog files.
+
+=== RSS Support ===
+
+The revision log also provides a RSS feed to monitor the changes.
+To subscribe to an RSS feed for a file or directory, open its
+revision log in the browser and click the orange 'XML' icon at the bottom
+of the page. For more information on RSS support in Trac, see TracRss.
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracRoadmap
@@ -0,0 +1,30 @@
+= The Trac Roadmap =
+[[TracGuideToc]]
+
+The roadmap provides a view on the [wiki:TracTickets ticket system] that helps planning and managing the future development of a project.
+
+== The Roadmap View ==
+
+Basically, the roadmap is just a list of future milestones. You can add a description to milestones (using WikiFormatting) describing main objectives, for example. In addition, tickets targeted for a milestone are aggregated, and the ratio between active and resolved tickets is displayed as a milestone progress bar.
+
+== The Milestone View ==
+
+It is possible to drill down into this simple statistic by viewing the individual milestone pages. By default, the active/resolved ratio will be grouped and displayed by component. You can also regroup the status by other criteria, such as ticket owner or severity. Ticket numbers are linked to [wiki:TracQuery custom queries] listing corresponding tickets.
+
+== Roadmap Administration ==
+
+It is possible to add, modify and remove milestones using either TracAdmin or the web interface. 
+
+'''Note:''' Milestone descriptions can currently only be edited from the web interface. With appropriate permissions, you'll see buttons for milestone management on the roadmap and milestone pages.
+
+== iCalendar Support ==
+
+The Roadmap supports the [http://www.ietf.org/rfc/rfc2445.txt iCalendar] format to keep track of planned milestones and related tickets from your favorite calendar software. Calendar applications supporting the iCalendar specification include [http://www.apple.com/ical/ Apple iCal] for Mac OS X and the cross-platform [http://www.mozilla.org/projects/calendar/ Mozilla Calendar]. [http://kdepim.kde.org/components/korganizer.php Korganiser] (the calendar application of the [http://www.kde.org/ KDE] project) and [http://www.novell.com/de-de/products/desktop/features/evolution.html Evolution] also support iCalendar.
+
+To subscribe to the roadmap, copy the iCalendar link from the roadmap (found at the bottom of the page) and choose the "Subscribe to remote calendar" action (or similar) of your calendar application, and insert the URL just copied.
+
+'''Note:''' For tickets to be included in the calendar as tasks, you need to be logged in when copying the link. You will only see tickets assigned to yourself, and associated with a milestone.
+
+More information about iCalendar can be found at [http://en.wikipedia.org/wiki/ICalendar Wikipedia].
+----
+See also: TracTickets, TracReports, TracQuery, TracGuide
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracRss
@@ -0,0 +1,27 @@
+= Using RSS with Trac =
+[[TracGuideToc]]
+
+Several of the Trac modules support content syndication using the RSS (Really Simple Syndication) XML format.
+Using the RSS subscription feature in Trac, you can easily monitor progress of the project, a set of issues or even changes to a single file .
+
+Trac supports RSS feeds in:
+
+ * TracTimeline --  Use the RSS feed to '''subscribe to project events'''.[[br]]Monitor overall project progress in your favorite RSS reader.
+ * TracReports and TracQuery -- Allows syndication of report and ticket query results.[[br]]Be notified about important and relevant issue tickets.
+ * TracBrowser -- Syndication of file changes.[[br]]Stay up to date with changes to a specific file or directory.
+
+== How to access RSS data ==
+Anywhere in Trac where RSS is available, you should find a small orange '''XML''' icon, typically placed at the bottom of the page. Clicking the icon will access the RSS feed for that specific resource.
+
+'''Note:''' Different modules provide different data in their RSS feeds. Usually, the syndicated information corresponds to the current view. For example, if you click the RSS link on a report page, the feed will be based on that report. It might be explained by thinking of the RSS feeds as an ''alternate view of the data currently displayed''.
+
+== Links ==
+ * http://blogs.law.harvard.edu/tech/rss -- RSS 2.0 Specification
+ * http://www.mozilla.org/products/firefox/ -- Mozilla Firefox supports [http://www.mozilla.org/products/firefox/live-bookmarks.html live bookmarks] using RSS
+ * http://sage.mozdev.org -- Sage RSS and Atom feed aggregator for Mozilla Firefox
+ * http://www.rssreader.com/ -- Free and powerful RSS Reader for Windows
+ * http://liferea.sourceforge.net/ -- Open source GTK2 RSS Reader for Linux
+ * http://akregator.sourceforge.net/ -- Open source KDE RSS Reader (will be part of KDE-PIM 3.4)
+ * http://www.sharpreader.net/ -- A free RSS Reader written in .NET for Windows
+----
+See also: TracGuide, TracTimeline, TracReports, TracBrowser
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracSearch
@@ -0,0 +1,26 @@
+= Using Search =
+
+Trac has a built-in search engine to allow finding occurrences of keywords and substrings in wiki pages, tickets and changeset descriptions.
+
+Using the Trac search facility is straightforward and its interface should be familiar to most www users.
+
+Apart from the [search: Search module], you will also find a small search field above the navigation bar at all time. It provides convenient access to the search module from all pages.
+
+== Quickjumps ==
+For intermediate and advanced use, Trac has a useful way to quickly navigate to a given resource, named '''quickjumps'''.
+
+If you enter a [wiki:TracLinks TracLink] in the search field above the navigation bar, Trac will recognize this and assume you know where you're going. 
+
+For example:
+
+ * ![42] -- Opens change set 42
+ * !#42 -- Opens ticket number 42
+ * !{1} -- Opens report 1
+
+'''Note:''' ''This is a particularly useful feature to quickly navigate to a specific issue ticket or changeset.''
+
+=== Advanced: Disabling Quickjumps ===
+To disable the quickjump feature for a search keyword - for example when searching for occurences of the literal word !TracGuide - begin the query with an exclamation mark (!).
+
+----
+See also: TracGuide, TracLinks, TracQuery
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracStandalone
@@ -0,0 +1,76 @@
+= Tracd =
+
+Tracd is a lightweight standalone Trac web server. In most cases it's easier to setup and runs faster than the [wiki:TracCgi CGI script].
+
+== Pros ==
+
+ * Fewer dependencies: You don't need to install apache or any other web-server.
+ * Fast: Should be as fast as the [wiki:TracModPython mod_python] version (and much faster than the [wiki:TracCgi CGI]).
+
+== Cons ==
+
+ * Less features: Tracd implements a very simple web-server and is not as configurable as Apache HTTPD.
+ * No native HTTPS support: [http://www.rickk.com/sslwrap/ sslwrap] can be used instead,
+   or [http://lists.edgewall.com/archive/trac/2005-August/004381.html STUNNEL].
+
+== Usage examples ==
+
+A single project on port 8080. (http://localhost:8080/)
+{{{
+ $ tracd -p 8080 /path/to/project
+}}}
+With more than one project. (http://localhost:8080/project1/ and http://localhost:8080/project2/)
+{{{
+ $ tracd -p 8080 /path/to/project1 /path/to/project2
+}}}
+
+You can't have the last portion of the path identical between the projects since that's how trac keeps the URLs of the
+different projects unique. So if you use /project1/path/to and /project2/path/to, you will only see the second project.
+
+== Using Authentication ==
+
+Tracd provides support for both Basic and Digest authentication. The default is to use Digest; to use Basic authentication, replace `--auth` with `--basic-auth` in the examples below, and omit the realm.
+
+If the file `/path/to/users.htdigest` contain user accounts for project1 with the realm "mycompany.com", you'd use the following command-line to start tracd:
+{{{
+ $ tracd -p 8080 --auth project1,/path/to/users.htdigest,mycompany.com /path/to/project1
+}}}
+''Note that the project “name†passed to the `--auth` option is actually the base name of the project environment directory.""
+
+Of course, the digest file can be be shared so that it is used for more than one project:
+{{{
+ $ tracd -p 8080 \
+   --auth project1,/path/to/users.htdigest,mycompany.com \
+   --auth project2,/path/to/users.htdigest,mycompany.com \
+   /path/to/project1 /path/to/project2
+}}}
+
+== Generating Passwords Without Apache ==
+
+If you don't have Apache available, you can use this simple Python script to generate your passwords:
+
+{{{
+from optparse import OptionParser
+import md5
+
+# build the options
+usage = "usage: %prog [options]"
+parser = OptionParser(usage=usage)
+parser.add_option("-u", "--username",action="store", dest="username", type = "string",
+                  help="the username for whom to generate a password")
+parser.add_option("-p", "--password",action="store", dest="password", type = "string",
+                  help="the password to use")
+(options, args) = parser.parse_args()
+
+# check options
+if (options.username is None) or (options.password is None):
+   parser.error("You must supply both the username and password")
+   
+# Generate the string to enter into the htdigest file
+realm = 'trac'
+kd = lambda x: md5.md5(':'.join(x)).hexdigest()
+print ':'.join((options.username, realm, kd([options.username, realm, options.password])))
+}}}
+
+----
+See also: TracInstall, TracCgi, TracModPython, TracGuide
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracSupport
@@ -0,0 +1,16 @@
+= Trac Support =
+----
+'''Warning:''' the following instructions apply to the '''Trac web application itself''', ''not'' to the actual software being managed by this instance of Trac. Check the project's WikiStart page for pointers to the relevant support information.
+----
+
+Like in most [http://www.opensource.org/ open source projects], "free" Trac support is available primarily through the community itself, mainly through the [http://projects.edgewall.com/trac/wiki/MailingList mailing list] and the project wiki.
+
+There is also an [http://projects.edgewall.com/trac/wiki/IrcChannel IRC channel], where people might be able to help out. Much of the 'live' development discussions also happen there.
+
+Before you start a new support query, make sure you've done the appropriate searching:
+ * in the project's [http://projects.edgewall.com/trac/wiki/TracFaq FAQ]
+ * in past messages to the Trac [http://blog.gmane.org/gmane.comp.version-control.subversion.trac.general?set_user_css=http%3A%2F%2Fwww.edgewall.com%2Fcss%2Fgmane.css&do_set_user_css=t Mailing List]
+ * in the Trac ticket system, using either a [http://projects.edgewall.com/trac/search?q=&ticket=on&wiki=on full search] or a [http://projects.edgewall.com/trac/query?summary=~&keywords=~" ticket query].
+
+----
+See also: [http://projects.edgewall.com/trac/wiki/MailingList MailingList]
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracSyntaxColoring
@@ -0,0 +1,75 @@
+= Syntax Coloring of Source Code =
+Trac supports language-specific syntax highlighting of source code in [wiki:WikiFormatting wiki formatted] text and the [wiki:TracBrowser repository browser].
+
+To do this, Trac uses external libraries with support for a great number of programming languages.
+
+Currently Trac supports syntax coloring using one or more of the following packages:
+
+ * [http://people.ssh.fi/mtr/genscript/ GNU Enscript]
+ * [http://silvercity.sourceforge.net/ SilverCity]
+
+To activate syntax coloring, simply install either one (or more) of these packages. No additional configuration is required, however to modify the colors, have a look at `trac/htdocs/css/code.css`.
+
+When in use, Trac will automatically prioritize !SilverCity highlighting over Enscript if possible, (see note below). 
+
+If neither package is available, Trac will display the data as plain text. 
+
+'''Note:''' Enscript supports a greater number of languages, however !SilverCity is generally faster since it is a library and isn't executed in an external process.
+
+=== About SilverCity ===
+!SilverCity uses the lexer from [http://www.scintilla.org/ Scintilla]. Scintilla supports more languages than !SilverCity implements. If you want to add a language to !SilverCity supported by Scintilla, it's not very difficult. See [http://projects.edgewall.com/trac/wiki/SilverCityAddLanguage SilverCityAddLanguage] for some information how.
+
+
+== Syntax Coloring Support ==
+
+|| || !SilverCity || Enscript ||
+|| Ada      ||   || X ||
+|| Asm      ||   || X ||
+|| * ASP    || X || X ||
+|| * C      || X || X ||
+|| * C++    || X || X ||
+|| * Java   ||   || X ||
+|| Awk      ||   || X ||
+|| CSS      || X ||   ||
+|| Diff     ||   || X ||
+|| Eiffel   ||   || X ||
+|| Elisp    ||   || X ||
+|| Fortran  ||   || X ||
+|| Haskell  ||   || X ||
+|| HTML     || X || X ||
+|| IDL      ||   || X ||
+|| Javascript || X || X ||
+
+|| m4       ||   || X ||
+|| Makefile ||   || X ||
+|| Matlab   ||   || X ||
+|| Objective-C|| || X ||
+|| Pascal   ||   || X ||
+|| * Perl   || X || X ||
+|| * PHP    || X || X ||
+|| PSP      || X ||   ||
+|| Pyrex    ||   || X ||
+|| * Python || X || X ||
+|| * Ruby   || X || X (1) ||
+|| Scheme   ||   || X ||
+|| Shell    ||   || X ||
+|| SQL      || X || X ||
+|| Troff    ||   || X ||
+|| TCL      ||   || X ||
+|| Tex      ||   || X ||
+|| Verilog  ||   || X ||
+|| VHDL     ||   || X ||
+|| Visual Basic |||| X ||
+|| VRML     ||   || X ||
+|| XML      || X || X ||
+
+''(*) Supported as inline code blocks in [wiki:WikiFormatting Wiki text] using WikiProcessors.''
+
+''(1) Ruby highlighting is not included in the Enscript distribution.  Highlighting rules for Ruby can be obtained from: http://neugierig.org/software/ruby/
+
+== Extra Software ==
+ * GNU Enscript -- http://people.ssh.fi/mtr/genscript/
+ * !SilverCity -- http://silvercity.sf.net/
+
+----
+See also: WikiProcessors, WikiFormatting, TracWiki, TracBrowser
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracTickets
@@ -0,0 +1,111 @@
+= The Trac Ticket System =
+[[TracGuideToc]]
+
+The Trac issue database provides simple but effective tracking of issues and bugs within a project.
+
+As the central project management element of Trac, tickets are used for '''project tasks''', '''feature requests''', '''bug reports''' and '''software support issues'''. 
+
+As with the TracWiki, this subsystem has been designed with the goal of making user contribution and participation as simple as possible. It should be as easy as possible to report bugs, ask questions and suggest improvements.
+
+An issue is assigned to a person who must resolve it or reassign the ticket to someone else.
+All tickets can be edited, annotated, assigned, prioritized and discussed at any time.
+
+'''Note:''' To make full use of the ticket system, use it as an ''in bucket'' for ideas and tasks for your project, rather than just bug/fault reporting. 
+
+== Ticket Fields ==
+
+A  ticket contains the following information attributes:
+ 
+ * '''Reporter''' - The author of the ticket.
+ * '''Type''' - The nature of the ticket (for example, defect or enhancement request)
+
+ * '''Component''' - The project module or subsystem this ticket concerns.
+ * '''Version''' - Version of the project that this ticket pertains to.
+ * '''Keywords''' - Keywords that a ticket is marked with.  Useful for searching and report generation.
+
+ * '''Priority''' - The importance of this issue, ranging from ''trivial'' to ''blocker''.
+ * '''Milestone''' - When this issue should be resolved at the latest.
+ * '''Assigned to/Owner''' - Principal person responsible for handling the issue.
+ * '''Cc''' - A list of other associated people. ''Note that this does not imply responsiblity or any other policy.''
+ 
+ * '''Resolution''' - Reason for why a ticket was closed. One of {{{fixed}}}, {{{invalid}}}, {{{wontfix}}}, {{{duplicate}}}, {{{worksforme}}}.
+ * '''Status''' - What is the current status? One of {{{new}}}, {{{assigned}}}, {{{closed}}}, {{{reopened}}}.
+ * '''Summary''' - A brief description summarizing the problem or issue.
+ * '''Description''' - The body of the ticket. A good description should be specific, descriptive and to the point.
+
+'''Note:''' Versions of Trac prior to 0.9 did not have the ''type'' field, but instead provided a ''severity'' field and different default values for the ''priority'' field. This change was done to simplify the ticket model by removing the somewhat blurry distinction between ''priority'' and ''severity''. However, the old model is still available if you prefer it: just add/modify the default values of the ''priority'' and ''severity'', and optionally hide the ''type'' field by removing all the possible values through [wiki:TracAdmin trac-admin].
+
+
+== Changing and Commenting Tickets ==
+
+Once a ticket has been entered into Trac, you can at any time change the
+information by '''annotating''' the bug. This means changes and comments to
+the ticket are logged as a part of the ticket itself.
+
+When viewing a ticket, the history of changes will appear below the main ticket area.
+
+''In the Trac project, we use ticket comments to discuss issues and tasks. This makes
+understanding the motivation behind a design- or implementation choice easier,
+when returning to it later.''
+
+'''Note:''' An important feature is being able to use TracLinks and
+WikiFormatting in ticket descriptions and comments. Use TracLinks to refer to
+other issues, changesets or files to make your ticket more specific and easier
+to understand.
+
+'''Note:''' See TracNotification for how to configure email notifications of ticket changes.
+
+=== State Diagram ===
+http://projects.edgewall.com/trac/attachment/wiki/TracTickets/Trac%20Ticket%20State%20Chart%2020040607DF.png?format=raw
+
+
+== Default Values for Drop-Down Fields ==
+
+The option selected by default for the various drop-down fields can be set in [wiki:TracIni trac.ini], in the `[ticket]` section:
+
+ * `default_type`: Default ticket type
+ * `default_component`: Name of the component selected by default
+ * `default_version`: Name of the default version
+ * `default_milestone`: Name of the default milestone
+ * `default_priority`: Default priority value
+ * `default_severity`: Default severity value
+
+If any of these options are omitted, the default value will either be the first in the list, or an empty value, depending on whether the field in question is required to be set.
+
+
+== Hiding Fields and Adding Custom Fields ==
+
+Many of the default ticket fields can be hidden from the ticket web interface simply by removing all the possible values through [wiki:TracAdmin trac-admin]. This of course only applies to drop-down fields, such as ''type'', ''priority'', ''severity'', ''component'', ''version'' and ''milestone''.
+
+Trac also lets you add your own custom ticket fields. See TracTicketsCustomFields for more information.
+
+
+== Assign-to as Drop-Down List ==
+
+If the list of possible ticket owners is finite, you can change the ''assign-to'' ticket field from a text input to a drop-down list. This is done by setting the `restrict_owner` option of the `[ticket]` section in [wiki:TracIni trac.ini] to “trueâ€. In that case, Trac will use the list of all users who have logged in and set their email address to populate the drop-down field.
+
+''Note that this feature is '''still experimental as of version 0.9'''. There is no way to only display a subset of all known users as possible ticket owners. Nor is there a convenient way to remove emeritus users short of directly modifying the database.''
+
+
+== Preset Values for New Tickets ==
+
+To create a link to the new-ticket form filled with preset values, you need to call the `/newticket?` URL with variable=value separated by &. 
+
+Possible variables are :
+
+ * '''reporter''' - Name or email of the reporter
+ * '''summary''' - Summary line for the ticket
+ * '''description''' - Long description of the ticket
+ * '''component''' - The component droplist
+ * '''version''' - The version droplist
+ * '''severity''' - The severity droplist
+ * '''keywords''' - The keywords 
+ * '''priority''' - The priority droplist
+ * '''milestone''' - The milestone droplist
+ * '''owner''' - The person responsible for the ticket
+ * '''cc''' - The list of emails for notifying about the ticket change
+
+'''Example:''' ''/trac/newticket?summary=Compile%20Error&version=1.0&component=gui''
+
+
+See also:  TracGuide, TracWiki, TracTicketsCustomFields, TracNotification
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracTicketsCustomFields
@@ -0,0 +1,98 @@
+= Custom Ticket Fields =
+Trac supports adding custom, user-defined fields to the ticket module. Using custom fields, you can add typed, site-specific properties to tickets.
+
+== Configuration ==
+Configuring custom ticket fields is done in the [wiki:TracIni trac.ini] file. All field definitions should be under a section named `[ticket-custom]`.
+
+The syntax of each field definition is:
+{{{
+ FIELD_NAME = TYPE
+ (FIELD_NAME.OPTION = VALUE)
+ ...
+}}}
+The example below should help to explain the syntax.
+
+=== Available Field Types and Options ===
+ * '''text''': A simple (one line) text field.
+   * label: Descriptive label.
+   * value: Default value.
+   * order: Sort order placement. (Determines relative placement in forms.)
+ * '''checkbox''': A boolean value check box.
+   * label: Descriptive label.
+   * value: Default value (0 or 1).
+   * order: Sort order placement.
+ * '''select''': Drop-down select box. Uses a list of values.
+   * options: List of values, separated by '''|''' (vertical pipe).
+   * value: Default value (Item #, starting at 0).
+   * order: Sort order placement.
+ * '''radio''': Radio buttons. Essentially the same as '''select'''.
+   * label: Descriptive label.
+   * options: List of values, separated by '''|''' (vertical pipe).
+   * value: Default value (Item #, starting at 0).
+   * order: Sort order placement.
+ * '''textarea''': Multi-line text area.
+   * label: Descriptive label.
+   * value: Default text.
+   * cols: Width in columns.
+   * rows: Height in lines.
+   * order: Sort order placement.
+
+=== Sample Config ===
+{{{
+[ticket-custom]
+
+test_one = text
+test_one.label = Just a text box
+
+test_two = text
+test_two.label = Another text-box
+test_two.value = Just a default value
+
+test_three = checkbox
+test_three.label = Some checkbox
+test_three.value = 1
+
+test_four = select
+test_four.label = My selectbox
+test_four.options = one|two|third option|four
+test_four.value = 2
+
+test_five = radio
+test_five.label = Radio buttons are fun
+test_five.options = uno|dos|tres|cuatro|cinco
+test_five.value = 1
+
+test_six = textarea
+test_six.label = This is a large textarea
+test_six.value = Default text
+test_six.cols = 60
+test_six.rows = 30
+}}}
+
+''Note: To make an entering an option for a `select` type field optional, specify a leading `|` in the `fieldname.options` option.''
+
+=== Reports Involving Custom Fields ===
+
+The SQL required for TracReports to include custom ticket fields is relatively hard to get right. You need a `JOIN` with the `ticket_custom` field for every custom field that should be involved.
+
+The following example includes a custom ticket field named `progress` in the report:
+{{{
+#!sql
+SELECT p.value AS __color__,
+   id AS ticket, summary, component, version, milestone, severity,
+   (CASE status WHEN 'assigned' THEN owner||' *' ELSE owner END) AS owner,
+   time AS created,
+   changetime AS _changetime, description AS _description,
+   reporter AS _reporter,
+  (CASE WHEN c.value = '0' THEN 'None' ELSE c.value END) AS progress
+  FROM ticket t
+     LEFT OUTER JOIN ticket_custom c ON (t.id = c.ticket AND c.name = 'progress')
+     JOIN enum p ON p.name = t.priority AND p.type='priority'
+  WHERE status IN ('new', 'assigned', 'reopened')
+  ORDER BY p.value, milestone, severity, time
+}}}
+
+Note in particular the `LEFT OUTER JOIN` statement here.
+
+----
+See also: TracTickets, TracIni
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracTimeline
@@ -0,0 +1,24 @@
+= The Trac Timeline =
+[[TracGuideToc]]
+
+The timeline provides a historic view of the project in a single report.
+
+It lists all Trac events that have occured in chronological order, a
+brief description of each event and if applicable, the person responsible for
+the change.
+
+The timeline lists these kinds of events:
+ * '''Wiki page events''' -- Creation and changes
+ * '''Ticket events''' -- Creation and resolution/closing (and optionally other changes)
+ * '''Source code changes ''' -- Repository check-ins
+ * '''Milestone ''' -- Milestone completed
+
+Each event entry provides a hyperlink to the specific event in question, as well as
+a brief excerpt of the actual comment or text, if available.
+
+== RSS Support ==
+
+The Timeline module supports subscription using RSS 2.0 syndication. To subscribe to project events, click orange '''XML''' icon at the bottom of the page. See TracRss for more information on RSS support in Trac.
+
+----
+See also: TracGuide, TracWiki, WikiFormatting, TracRss, TracNotification
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracUnicode
@@ -0,0 +1,74 @@
+= Unicode Support in Trac =
+Trac stores all text using UTF-8 encoding, including text in tickets and wiki pages. 
+
+As such, it supports most (all?) commonly used character encodings.
+
+
+== Examples ==
+Please keep sorted order when you add an entry.
+
+=== Arabic ===
+تراك يقوم بحÙظ كل الكلمات باستخدام صيغة UTF-8ØŒ بما ÙÙŠ ذلك الكلمات المستخدمة ÙÙŠ صÙحات  التيكت والويكي.
+
+=== Bulgarian ===
+БългарÑкиÑÑ‚ език работи ли?
+
+=== ÄŒesky ===
+Čeština v kódování UTF-8, žádný problém.
+
+=== Chinese ===
+Traditional: ''ç¹é«”中文, 許功蓋會育''; Simplified: ''简体中文,许功盖会育''
+
+=== English ===
+Yes indeed, Trac supports English. Fully.
+
+=== Français ===
+''Il est possible d'écrire en Français : à, ç, û, ...''
+
+=== German ===
+Trac-Wiki muß auch deutsche Umlaute richtig anzeigen: ö, ä, ü, ...
+
+=== Greek ===
+Τα Ελληνικά υποστηÏίζονται επαÏκώς επίσης.
+
+=== Hebrew ===
+×× ×™ יכול ל×כול זכוכית וזה ×œ× ×ž×–×™×§ לי
+
+=== Icelandic ===
+''Ævar sagði við ömmu sína: Sjáðu hvað ég er stór!''
+
+=== Japanese ===
+''漢字 ã²ã‚‰ãŒãª カタカナ ハï¾ï½¶ï½¸ï½¶ï¾…''
+
+=== Korean ===
+''ì´ë²ˆì—는 한글로 ì¨ë³´ê² ìŠµë‹ˆë‹¤. 잘 ë³´ì´ë‚˜ìš”?''
+
+=== Persian (Farsi) ===
+این یک متن Ùارسی است ولی از Ú†Ù¾ به راست
+
+=== Polish ===
+Pchnąć w tę łódź jeża lub ośm skrzyń fig
+
+=== Portuguese ===
+É possível guardar caracteres especias da língua portuguesa, incluindo o símbolo da moeda européia '€', trema 'ü', crase 'à', agudos 'áéíóú', circunflexos 'âêô', til 'ãõ', cedilha 'ç', ordinais 'ªº', grau '°¹²³'.
+
+=== Russian ===
+Проверка руÑÑкого Ñзыка: кажетÑÑ Ñ€Ð°Ð±Ð¾Ñ‚Ð°ÐµÑ‚...
+
+=== Serbian ===
+Podržan, uprkos Äinjenici da se za njegovo pisanje koriste чак два алфабета.
+
+=== Slovenian ===
+Ta suhi Å¡kafec puÅ¡Äa vodo že od nekdaj!
+
+=== Spanish ===
+Esto es un pequeño texto en Español, ahora una con acentó
+
+=== Swedish ===
+''Räven raskar över isen med luva på.''
+
+=== Thai ===
+Trac à¹à¸ªà¸”งภาษาไทยได้อย่างถูà¸à¸•à¹‰à¸­à¸‡!
+
+=== Ukrainian ===
+Перевірка українÑької мови...
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracUpgrade
@@ -0,0 +1,99 @@
+= Upgrade Instructions =
+[[TracGuideToc]]
+
+A Trac environment sometimes needs to be upgraded before it can be used with a new version of Trac. This document describes the steps necessary to upgrade an environment.
+
+ '''Note''': ''Environment upgrades are not necessary for minor version releases unless otherwise noted. For example, there's no need to upgrade a Trac environment created with (or upgraded) 0.8.0 when installing 0.8.4 (or any other 0.8.x release).''
+
+== General Instructions ==
+
+Typically, there are four steps involved in upgrading to a newer version of Trac:
+
+=== Update the Trac Code ===
+
+Get the new version of Trac, either by downloading an offical release package or by checking it out from the [http://projects.edgewall.com/trac/wiki/SubversionRepository Subversion repository].
+
+If you have a source distribution, you need to run
+{{{
+python setup.py install
+}}}
+
+to install the new version. If you've downloaded the Windows installer, you execute it, and so on.
+
+In any case, if you're doing a major version upgrade (such as from 0.8 to 0.9), it is ''highly'' recommended that you first remove the existing Trac code. To do this, you need to delete the `trac` directory from the Python `lib/site-packages` directory. You may also want to remove the Trac `cgi-bin`, `htdocs` and `templates` directories that are commonly found in a directory called `share/trac` (the exact location depends on your platform).
+
+=== Upgrade the Trac Environment ===
+
+Unless noted otherwise, upgrading between major versions (such as 0.8 and 0.9) involves changes to the database schema, and possibly the layout of the [wiki:TracEnvironment environment directory]. Fortunately, Trac provides automated upgrade scripts to ease the pain. These scripts are run via [wiki:TracAdmin trac-admin]:
+{{{
+trac-admin /path/to/projenv upgrade
+}}}
+
+This command will do nothing if the environment is already up-to-date.
+
+Note that if you are using a PostgreSQL database, this command will fail with the message that the environment can only be backed up when you use an SQLite database. This means that you will have to backup the repository and the database manually. Then, to perform the actual upgrade, run:
+{{{
+trac-admin /path/to/projenv upgrade --no-backup
+}}}
+
+=== Update the Trac Documentation ===
+
+Every [wiki:TracEnvironment Trac environment] includes a copy of the Trac documentation for the installed version. As you probably want to keep the included documentation in sync with the installed version of Trac, [wiki:TracAdmin trac-admin] provides a command to upgrade the documentation:
+{{{
+trac-admin /path/to/projenv wiki upgrade
+}}}
+
+Note that this procedure will of course leave your `WikiStart` page intact.
+
+=== Restart the Web Server ===
+
+In order to reload the new Trac code you will need to restart your web server (note this is not necessary for [wiki:TracCgi CGI]).
+
+== Specific Versions ==
+
+The following sections discuss any extra actions that may need to be taken to upgrade to specific versions of Trac.
+
+== From 0.9-beta to 0.9 ==
+
+If inclusion of the static resources (style sheets, javascript, images) is not working, check the value of the `htdocs_location` in trac.ini. For [wiki:TracModPython mod_python], [wiki:TracStandalone Tracd] and [wiki:TracFastCgi FastCGI], you can simply remove the option altogether. For [wiki:TracCgi CGI], you should fix it to point to the URL you mapped the Trac `htdocs` directory to (although you can also remove it and then [wiki:TracCgi#MappingStaticResources map the static resources]). If you're still having problems after removing the option, check the paths in the `trac/siteconfig.py` file and fix them if they're incorrect.
+
+If you've been using plugins with a beta release of Trac 0.9, or have disabled some of the built-in components, you might have to update the rules for disabling/enabling components in [wiki:TracIni trac.ini]. In particular, globally installed plugins now need to be enabled explicitly. See TracPlugins and TracIni for more information.
+
+If you want to enable the display of all ticket changes in the timeline (the “Ticket Details†option), you now have to explicitly enable that in [wiki:TracIni trac.ini], too:
+
+{{{
+[timeline]
+ticket_show_details = true
+}}}
+
+== From 0.8.x to 0.9 ==
+
+[wiki:TracModPython mod_python] users will also need to change the name of the mod_python handler in the Apache HTTPD configuration:
+{{{
+   from: PythonHandler trac.ModPythonHandler
+   to:   PythonHandler trac.web.modpython_frontend
+}}}
+
+If you have [http://initd.org/tracker/pysqlite PySQLite] 2.x installed, Trac will now try to open your SQLite database using the SQLite 3.x file format. The database formats used by SQLite 2.8.x and SQLite 3.x are incompatible. If you get an error like ''“file is encrypted or is not a databaseâ€'' after upgrading, then you must convert your database file.
+
+To do this, you need to have both SQLite 2.8.x and SQLite 3.x installed (they have different filenames so can coexist on the same system). Then use the following commands:
+{{{
+ $ mv trac.db trac2.db
+ $ sqlite trac2.db .dump | sqlite3 trac.db
+}}}
+
+After testing that the conversion was successful, the `trac2.db` file can be deleted. For more information on the SQLite upgrade see http://www.sqlite.org/version3.html.
+
+== From 0.7.x to 0.8 ==
+
+0.8 adds a new roadmap feature which requires additional permissions. While a
+fresh installation will by default grant ROADMAP_VIEW and MILESTONE_VIEW
+permissions to anonymous, these permissions have to be granted manually when
+upgrading:
+{{{
+ $ trac-admin /path/to/projectenv permission add anonymous MILESTONE_VIEW
+ $ trac-admin /path/to/projectenv permission add anonymous ROADMAP_VIEW
+}}}
+
+-----
+See also: TracGuide, TracInstall
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/TracWiki
@@ -0,0 +1,25 @@
+= The Trac Wiki Engine =
+[[TracGuideToc]]
+
+Trac has a built-in wiki engine, used for text and documentation throughout the system. WikiFormatting is used in [wiki:TitleIndex wiki pages], [wiki:TracTickets tickets] and [wiki:TracChangeset check-in log messages].  This allows for formatted text and hyperlinks in and between all Trac modules.
+
+Editing wiki text is easy, using any web browser and a simple formatting system (see WikiFormatting), rather than more complex markup languages like HTML.  The reasoning behind its design is that HTML, with its large collection of nestable tags, is too complicated to allow fast-paced editing, and distracts from the actual content of the pages. Note though that Trac also supports [wiki:WikiHtml HTML] and [wiki:WikiRestructuredText reStructuredText] as alternative markup formats.
+
+The main goal of the wiki is making editing text easier and ''encourage'' people to contribute and annotate text content for a project. 
+
+The wiki itself does not enforce any structure, but rather resembles a stack of empty paper sheets, where you can organize information and documentation as you see fit, and later reorganize if necessary. 
+
+For more help on editing wiki text, see:
+ * WikiFormatting
+ * WikiPageNames
+ * WikiNewPage
+ * TracLinks
+ * WikiMacros
+ * WikiProcessors
+
+If you want to practice editing, please use the SandBox.
+
+Some more information about wiki on the web:
+ * http://wikipedia.org/wiki/Wiki
+ * http://c2.com/cgi/wiki?WikiHistory
+ * http://www.usemod.com/cgi-bin/mb.pl?WhyWikiWorks
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/WikiDeletePage
@@ -0,0 +1,10 @@
+= Deleting a Wiki Page =
+
+Existing wiki pages can be completely deleted using the ''Delete Page'' or the ''Delete this Version'' buttons at the bottom of the wiki page. These buttons are only visible for users with `WIKI_DELETE` permissions.
+
+'''Note:''' This is an irreversible operation.
+
+In general, it is recommended to create redirection pages instead of completely deleting an old page, as to not frustrate the visitor with broken links when coming to the site from a search engine. A redirection page is a short page that  contains a link such as  “See !SomeOtherPageâ€. However, deleting specific versions or even complete pages can make sense to remove spam or other abuse.
+
+----
+See also: TracWiki, TracPermissions
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/WikiFormatting
@@ -0,0 +1,341 @@
+= WikiFormatting =
+[[TracGuideToc]]
+
+Wiki markup is a core feature in Trac, tightly integrating all the other parts of Trac into a flexible and powerful whole.
+
+Trac has a built in small and powerful wiki rendering engine. This wiki engine implements an ever growing subset of the commands from other popular Wikis,
+especially [http://moinmoin.wikiwikiweb.de/ MoinMoin]. 
+
+
+This page demonstrates the formatting syntax available anywhere WikiFormatting is allowed.
+
+
+== Font Styles ==
+
+The Trac wiki supports the following font styles:
+{{{
+ * '''bold'''
+ * ''italic''
+ * '''''bold italic'''''
+ * __underline__
+ * {{{monospace}}} or `monospace`
+ * ~~strike-through~~
+ * ^superscript^ 
+ * ,,subscript,,
+}}}
+
+Display:
+ * '''bold'''
+ * ''italic''
+ * '''''bold italic'''''
+ * __underline__
+ * {{{monospace}}} or `monospace`
+ * ~~strike-through~~
+ * ^superscript^ 
+ * ,,subscript,,
+
+Note that the `{{{...}}}` and {{{`...`}}} commands not only select a monospace font, but also treat their content as verbatim text, meaning that no further wiki processing is done on this text.
+
+== Headings ==
+
+You can create heading by starting a line with one up to five ''equal'' characters ("=")
+followed by a single space and the headline text. The line should end with a space 
+followed by the same number of ''='' characters.
+
+Example:
+{{{
+= Heading =
+== Subheading ==
+=== About ''this'' ===
+}}}
+
+Display:
+= Heading =
+== Subheading ==
+=== About ''this'' ===
+
+
+== Paragraphs ==
+
+A new text paragraph is created whenever two blocks of text are separated by one or more empty lines.
+
+A forced line break can also be inserted, using:
+{{{
+Line 1[[BR]]Line 2
+}}}
+Display:
+
+Line 1[[BR]]Line 2
+
+
+== Lists ==
+
+The wiki supports both ordered/numbered and unordered lists.
+
+Example:
+{{{
+ * Item 1
+   * Item 1.1
+ * Item 2
+
+ 1. Item 1
+   1. Item 1.1
+ 1. Item 2
+}}}
+
+Display:
+ * Item 1
+   * Item 1.1
+ * Item 2
+
+ 1. Item 1
+   1. Item 1.1
+ 1. Item 2
+
+Note that there must be one or more spaces preceding the list item markers, otherwise the list will be treated as a normal paragraph.
+
+
+== Definition Lists ==
+
+The wiki also supports definition lists.
+
+Example:
+{{{
+ llama::
+   some kind of mammal, with hair
+ ppython::
+   some kind of reptile, without hair
+   (can you spot the typo?)
+}}}
+
+Display:
+ llama::
+   some kind of mammal, with hair
+ ppython::
+   some kind of reptile, without hair
+   (can you spot the typo?)
+
+Note that you need a space in front of the defined term.
+
+
+== Preformatted Text ==
+
+Block containing preformatted text are suitable for source code snippets, notes and examples. Use three ''curly braces'' wrapped around the text to define a block quote. The curly braces need to be on a separate line.
+  
+Example:
+{{{
+ {{{
+  def HelloWorld():
+      print "Hello World"
+ }}}
+}}}
+
+Display:
+{{{
+ def HelloWorld():
+     print "Hello World"
+}}}
+
+
+== Blockquotes ==
+
+In order to mark a paragraph as blockquote, indent that paragraph with two spaces.
+
+Example:
+{{{
+  This text is a quote from someone else.
+}}}
+
+Display:
+  This text is a quote from someone else.
+
+== Tables ==
+
+Simple tables can be created like this:
+{{{
+||Cell 1||Cell 2||Cell 3||
+||Cell 4||Cell 5||Cell 6||
+}}}
+
+Display:
+||Cell 1||Cell 2||Cell 3||
+||Cell 4||Cell 5||Cell 6||
+
+Note that more complex tables can be created using
+[wiki:WikiRestructuredText#BiggerReSTExample reStructuredText].
+
+
+== Links ==
+
+Hyperlinks are automatically created for WikiPageNames and URLs. !WikiPageLinks can be disabled by prepending an exclamation mark "!" character, such as {{{!WikiPageLink}}}.
+
+Example:
+{{{
+ TitleIndex, http://www.edgewall.com/, !NotAlink
+}}}
+
+Display:
+ TitleIndex, http://www.edgewall.com/, !NotAlink
+
+Links can be given a more descriptive title by writing the link followed by a space and a title and all this inside square brackets.  If the descriptive title is omitted, then the explicit prefix is disguarded, unless the link is an external link. This can be useful for wiki pages not adhering to the WikiPageNames convention.
+
+Example:
+{{{
+ * [http://www.edgewall.com/ Edgewall Software]
+ * [wiki:TitleIndex Title Index]
+ * [wiki:ISO9000]
+}}}
+
+Display:
+ * [http://www.edgewall.com/ Edgewall Software]
+ * [wiki:TitleIndex Title Index]
+ * [wiki:ISO9000]
+
+
+=== Trac Links ===
+
+Wiki pages can link directly to other parts of the Trac system. Pages can refer to tickets, reports, changesets, milestones, source files and other Wiki pages using the following notations:
+{{{
+ * Tickets: #1 or ticket:1
+ * Reports: {1} or report:1
+ * Changesets: r1, [1] or changeset:1
+ * Revision Logs: r1:3, [1:3] or log:branches/0.8-stable#1:3
+ * Wiki pages: CamelCase or wiki:CamelCase
+ * Milestones: milestone:1.0 or milestone:"End-of-days Release"
+ * Files: source:trunk/COPYING
+ * Attachments: attachment:"file name.doc"
+ * A specific file revision: source:/trunk/COPYING#200
+ * A filename with embedded space: source:"/trunk/README FIRST"
+}}}
+
+Display:
+ * Tickets: #1 or ticket:1
+ * Reports: {1} or report:1
+ * Changesets: r1, [1] or changeset:1
+ * Revision Logs: r1:3, [1:3] or log:branches/0.8-stable#1:3
+ * Wiki pages: CamelCase or wiki:CamelCase
+ * Milestones: milestone:1.0 or milestone:"End-of-days Release"
+ * Files: source:trunk/COPYING
+ * Attachments: attachment:"file name.doc"
+ * A specific file revision: source:/trunk/COPYING#200
+ * A filename with embedded space: source:"/trunk/README FIRST"
+
+See TracLinks for more in-depth information.
+
+
+== Escaping Links and WikiPageNames ==
+
+You may avoid making hyperlinks out of TracLinks by preceding an expression with a single "!" (exclamation mark).
+
+Example:
+{{{
+ !NoHyperLink
+ !#42 is not a link
+}}}
+
+Display:
+ !NoHyperLink
+ !#42 is not a link
+
+
+== Images ==
+
+Urls ending with `.png`, `.gif` or `.jpg` are automatically interpreted as image links, and converted to `<img>` tags.
+
+Example:
+{{{
+http://www.edgewall.com/gfx/trac_example_image.png
+}}}
+
+Display:
+
+http://www.edgewall.com/gfx/trac_example_image.png
+
+However, this doesn't give much control over the display mode. This way of inserting images is deprecated in favor of the more powerful `Image` macro (see WikiMacros).
+
+
+== Macros ==
+
+Macros are ''custom functions'' to insert dynamic content in a page.
+
+Example:
+{{{
+ [[Timestamp]]
+}}}
+
+Display:
+ [[Timestamp]]
+
+See WikiMacros for more information, and a list of installed macros.
+
+
+== Processors ==
+
+Trac supports alternative markup formats using WikiProcessors. For example, processors are used to write pages in 
+[wiki:WikiRestructuredText reStructuredText] or [wiki:WikiHtml HTML]. 
+
+Example 1:
+{{{
+#!html
+<pre class="wiki">{{{
+#!html
+&lt;h1 style="text-align: right; color: blue"&gt;HTML Test&lt;/h1&gt;
+}}}</pre>
+}}}
+
+Display:
+{{{
+#!html
+<h1 style="text-align: right; color: blue">HTML Test</h1>
+}}}
+
+Example:
+{{{
+#!html
+<pre class="wiki">{{{
+#!python
+class Test:
+    def __init__(self):
+        print "Hello World"
+if __name__ == '__main__':
+   Test()
+}}}</pre>
+}}}
+
+Display:
+{{{
+#!python
+class Test:
+    def __init__(self):
+        print "Hello World"
+if __name__ == '__main__':
+   Test()
+}}}
+
+Perl:
+{{{
+#!perl
+my ($test) = 0;
+if ($test > 0) {
+echo "hello";
+}
+}}}
+
+See WikiProcessors for more information.
+
+
+== Miscellaneous ==
+
+Four or more dashes will be replaced by a horizontal line (<HR>)
+
+Example:
+{{{
+ ----
+}}}
+
+Display:
+----
+
+
+----
+See also: TracLinks, TracGuide, WikiHtml, WikiMacros, WikiProcessors, TracSyntaxColoring.
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/WikiHtml
@@ -0,0 +1,31 @@
+= Using HTML in Wiki Text =
+
+Trac supports inserting HTML into any wiki context, accomplished using the HTML [wiki:WikiProcessors WikiProcessor].
+
+HTML support is built-in, and does not require installing any additional packages.
+
+== How to Use HTML ==
+To inform the wiki engine that a block of text should be treated as HTML, use the ''html'' processor. 
+
+This example should explain:
+{{{
+#!html
+<pre class="wiki">{{{
+#!html
+&lt;h1 style="text-align: right; color: blue"&gt;HTML Test&lt;/h1&gt;
+}}}</pre>
+}}}
+
+Results in:
+{{{
+#!html
+<h1 style="text-align: right; color: blue">HTML Test</h1>
+}}}
+
+== More Information ==
+
+ * http://www.w3.org/ -- World Wide Web Consortium
+ * http://www.w3.org/MarkUp/ -- HTML Markup Home Page
+
+----
+See also:  WikiProcessors, WikiFormatting, WikiRestructuredText
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/WikiMacros
@@ -0,0 +1,58 @@
+=  Wiki Macros =
+Trac macros are plugins to extend the Trac engine with custom 'functions' written in Python. A macro inserts dynamic HTML data in any context supporting WikiFormatting.
+
+Another kind of macros are WikiProcessors. They typically deal with alternate markup formats and representation of larger blocks of information (like source code highlighting).
+
+== Using Macros ==
+Macro calls are enclosed in two ''square brackets''. Like python functions, macros can also have arguments, a comma separated list within parentheses. 
+
+=== Examples ===
+
+{{{
+ [[Timestamp]]
+}}}
+Display:
+ [[Timestamp]]
+
+{{{
+ [[HelloWorld(Testing)]]
+}}}
+Display:
+ [[HelloWorld(Testing)]]
+
+== Available Macros ==
+
+''Note that the following list will only contain the macro documentation if you've not enabled `-OO` optimizations, or not set the `PythonOptimize` option for [wiki:TracModPython mod_python].''
+
+[[MacroList]]
+
+== Macros from around the world ==
+The [http://projects.edgewall.com/trac/ Trac Project] has a section dedicated to user-contributed macros, [http://projects.edgewall.com/trac/wiki/MacroBazaar MacroBazaar]. If you're looking for new macros, or have written new ones to share with the world, don't hesitate adding it to the [http://projects.edgewall.com/trac/wiki/MacroBazaar MacroBazaar] wiki page.
+
+----
+
+== Developing Custom Macros ==
+Macros, like Trac itself, are written in the [http://www.python.org/ Python programming language]. They are very simple modules, identified by the filename and should contain a single ''entry point'' function. Trac will display the returned data inserted into the HTML where the macro was called.
+
+It's easiest to learn from an example:
+{{{
+#!python
+# MyMacro.py -- The world's simplest macro
+
+def execute(hdf, args, env):
+    return "Hello World called with args: %s" % args
+}}}
+
+You can also use the environment (`env`) object, for example to access configuration data and the database, for example:
+{{{
+#!python
+def execute(hdf, txt, env):
+    return env.get_config('trac', 'repository_dir')
+}}}
+
+Note that since version 0.9, wiki macros can also be written as TracPlugins. This gives them some capabilities than “classic†macros do not have, such as directly access the HTTP request.
+
+For more information about developing macros, see the [http://projects.edgewall.com/trac/wiki/TracDev development resources] on the main project site.
+
+----
+See also:  WikiProcessors, WikiFormatting, TracGuide
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/WikiNewPage
@@ -0,0 +1,10 @@
+= Steps to Add a New Wiki Page =
+
+ 1. Choose a name for your new page. See WikiPageNames for naming conventions.
+ 1. Edit an existing page and add a hyperlink to your new page. Save your changes.
+ 1. Follow the link you created to take you to the new page. Trac will display a "describe !PageName here" message.
+ 1. Click the "Edit this page" button to edit and add content to your new page. Save your changes.
+ 1. All done. Your new page is published.
+
+
+See also: TracWiki, WikiFormatting, TracLinks, WikiDeletePage 
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/WikiPageNames
@@ -0,0 +1,22 @@
+= Wiki Page Names =
+
+Wiki page names are written using CamelCase. Within a wiki text, any word in CamelCase automatically becomes a hyperlink to the wiki page with that same name.
+
+Page names must follow these rules:
+
+ 1. The name must consist of '''alphabetic characters only'''. No digits, spaces, punctuation, or underscores are allowed.
+ 1. A name must have at least two capital letters.
+ 1. The first character must be capitalized.
+ 1. Every capital letter must be followed by one or more lower-case letters. 
+ 1. The use of slash ( / ) is permitted to create a hierarchy inside the wiki.  (See !SubWiki and !ParentWiki macros in the [http://projects.edgewall.com/trac/wiki/MacroBazaar MacroBazaar] which provide a way to list all sub-entries and a link up the hierarchy respectively.)
+
+If you want to create a wiki page that doesn't follow CamelCase rules you could use the following syntax:
+{{{
+[wiki:Wiki_page]
+}}}
+
+This will be rendered as:
+ [wiki:Wiki_page]
+
+----
+See also: WikiNewPage, WikiFormatting, TracWiki
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/WikiProcessors
@@ -0,0 +1,125 @@
+= Wiki Processors =
+Processors are WikiMacros designed to provide alternative markup formats for the Trac Wiki engine. Processors can be thought of as ''macro functions to process user-edited text''. 
+
+The wiki engine uses processors to allow using [wiki:WikiRestructuredText Restructured Text] and [wiki:WikiHtml raw HTML] in any wiki text throughout Trac.
+
+== Using Processors ==
+To use a processor on a block of text, use a wiki blockquote, selecting a processor by name using ''shebang notation'' (#!), familiar to most UNIX users from scripts.
+
+'''Example 1''' (''inserting raw HTML in a wiki text''):
+
+{{{
+#!html
+<pre class="wiki">{{{
+#!html
+&lt;h1 style="color: orange"&gt;This is raw HTML&lt;/h1&gt;
+}}}</pre>
+}}}
+
+'''Results in:'''
+{{{
+#!html
+<h1 style="color: orange">This is raw HTML</h1>
+}}}
+
+----
+
+'''Example 2''' (''inserting Restructured Text in wiki text''):
+
+{{{
+#!html
+<pre class="wiki">{{{
+#!rst
+A header
+--------
+This is some **text** with a footnote [*]_.
+
+.. [*] This is the footnote.
+}}}</pre>
+}}}
+
+'''Results in:'''
+{{{
+#!rst
+A header
+--------
+This is some **text** with a footnote [*]_.
+
+.. [*] This is the footnote.
+}}}
+----
+'''Example 3''' (''inserting a block of C source code in wiki text''):
+
+{{{
+#!html
+<pre class="wiki">{{{
+#!c
+int main(int argc, char *argv[])
+{
+  printf("Hello World\n");
+  return 0;
+}
+}}}</pre>
+}}}
+
+'''Results in:'''
+{{{
+#!c
+int main(int argc, char *argv[])
+{
+  printf("Hello World\n");
+  return 0;
+}
+}}}
+
+----
+
+== Available Processors ==
+The following processors are included in the Trac distribution:
+ * '''html''' -- Insert custom HTML in a wiki page. See WikiHtml.
+ * '''rst''' -- Trac support for Restructured Text. See WikiRestructuredText.
+ * '''textile''' -- Supported if  [http://dealmeida.net/projects/textile/ Textile] is installed.
+
+=== Code Highlighting Support ===
+Trac includes processors to provide inline [wiki:TracSyntaxColoring syntax highlighting] for the following languages:
+ * '''c''' -- C
+ * '''cpp''' -- C++
+ * '''python''' -- Python
+ * '''perl''' -- Perl
+ * '''ruby''' -- Ruby
+ * '''php''' -- PHP
+ * '''asp''' --- ASP
+ * '''sql''' -- SQL
+ * '''xml''' -- XML
+'''Note:''' ''Trac relies on external software packages for syntax coloring. See TracSyntaxColoring for more info.''
+
+By using the MIME type as processor, it is possible to syntax-highlight the same languages that are supported when browsing source code. For example, you can write:
+{{{
+{{{
+#!text/html
+<h1>text</h1>
+}}}
+}}}
+
+The result will be syntax highlighted HTML code. The same is valid for all other mime types supported.
+
+
+For more processor macros developed and/or contributed by users, visit: 
+ * [http://projects.edgewall.com/trac/wiki/ProcessorBazaar ProcessorBazaar]
+ * [http://projects.edgewall.com/trac/wiki/MacroBazaar MacroBazaar]
+
+
+== Advanced Topics: Developing Processor Macros ==
+Developing processors is no different than WikiMacros. In fact they work the same way, only the usage syntax differs. See WikiMacros for more information.
+
+'''Example:''' (''Restructured Text Processor''):
+{{{
+from docutils.core import publish_string
+
+def execute(hdf, text, env):
+    html = publish_string(text, writer_name = 'html')
+    return html[html.find('<body>')+6:html.find('</body>')].strip()
+}}}
+
+----
+See also: WikiMacros, WikiHtml, WikiRestructuredText, TracSyntaxColoring, WikiFormatting, TracGuide
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/WikiRestructuredText
@@ -0,0 +1,173 @@
+= reStructuredText Support in Trac =
+
+Trac supports using ''reStructuredText'' (RST) as an alternative to wiki markup in any context WikiFormatting is used.
+
+From the reStucturedText webpage:
+ "''reStructuredText is an easy-to-read, what-you-see-is-what-you-get plaintext markup syntax and parser   system. It is useful for in-line program documentation (such as Python docstrings), for quickly creating  simple web pages, and for standalone documents. reStructuredText is designed for extensibility for  specific application domains. ''"
+
+=== Requirements ===
+Note that to activate RST support in Trac, the python docutils package must be installed. 
+If not already available on your operating system, you can download it at the [http://docutils.sourceforge.net/rst.html RST Website].
+
+=== More information on RST ===
+
+ * reStructuredText Website -- http://docutils.sourceforge.net/rst.html
+ * RST Quick Reference -- http://docutils.sourceforge.net/docs/rst/quickref.html
+
+----
+
+== Using RST in Trac ==
+To specify that a block of text should be parsed using RST, use the ''rst'' processor. 
+
+=== TracLinks in reStructuredText ===
+
+ * Trac provides a custom RST reference-directive 'trac' to allow TracLinks from within RST text.
+
+ Example:
+ {{{
+ {{{
+ #!rst
+ This is a reference to |a ticket|
+
+ .. |a ticket| trac:: #42
+ }}}
+ }}}
+
+ For a complete example of all uses of the ''trac''-directive, please see WikiRestructuredTextLinks. 
+
+
+ * Trac allows an even easier way of creating TracLinks in RST, using the custom '':trac:'' link naming scheme.
+
+ Example:
+ {{{
+ {{{
+ #!rst
+ This is a reference to ticket `#12`:trac:
+
+ To learn how to use Trac, see `TracGuide`:trac:
+ }}}
+ }}}
+
+=== Syntax highlighting in reStructuredText ===
+
+There is a directive for doing TracSyntaxColoring in ReST as well. The directive is called
+code-block
+
+Example
+
+{{{
+{{{
+#!rst
+
+.. code-block:: python
+
+ class Test:
+
+    def TestFunction(self):
+        pass
+
+}}}
+}}}
+
+Will result in the below.
+
+{{{
+#!rst
+
+.. code-block:: python
+
+ class Test:
+
+    def TestFunction(self):
+        pass
+
+}}}
+
+=== WikiMacros in reStructuredText ===
+
+For doing WikiMacros in ReST you use the same directive as for syntax highlightning i.e
+code-block. To work you must use a version of trac that has #801 applied. 
+
+=== WikiMacro Example ===
+
+{{{
+{{{
+#!rst
+
+.. code-block:: HelloWorld
+ 
+   Something I wanted to say
+
+
+}}}
+}}}
+
+Will result in the below.
+
+[[HelloWorld(Something I wanted to say)]]
+
+
+=== Bigger ReST Example ===
+The example below should be mostly self-explanatory:
+{{{
+#!html
+<pre class="wiki">{{{
+#!rst
+FooBar Header
+=============
+reStructuredText is **nice**. It has its own webpage_.
+
+A table:
+
+=====  =====  ======
+   Inputs     Output
+------------  ------
+  A      B    A or B
+=====  =====  ======
+False  False  False
+True   False  True
+False  True   True
+True   True   True
+=====  =====  ======
+
+RST TracLinks
+-------------
+
+See also ticket `#42`:trac:.
+
+.. _webpage: http://docutils.sourceforge.net/rst.html
+}}}</pre>
+}}}
+
+
+Results in:
+{{{
+#!rst
+FooBar Header
+=============
+reStructuredText is **nice**. It has its own webpage_.
+
+A table:
+
+=====  =====  ======
+   Inputs     Output
+------------  ------
+  A      B    A or B
+=====  =====  ======
+False  False  False
+True   False  True
+False  True   True
+True   True   True
+=====  =====  ======
+
+RST TracLinks
+-------------
+
+See also ticket `#42`:trac:.
+
+.. _webpage: http://docutils.sourceforge.net/rst.html
+}}}
+
+
+----
+See also: WikiRestructuredTextLinks, WikiProcessors, WikiFormatting
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/WikiRestructuredTextLinks
@@ -0,0 +1,119 @@
+= TracLinks in reStructuredText =
+
+This document is for testing the ``..trac::`` directive. The page is written like
+
+{{{
+{{{
+#!rst 
+
+Examples
+...
+...
+
+}}}
+}}}
+
+
+This is a list of example uses of the ''trac'' directive, providing use of TracLinks in WikiRestructuredText.
+
+{{{
+#!rst
+
+Examples
+--------
+
+trac role
+=========
+Syntax is \`link\`\:trac: or :trac:\`link\`, and could be put anywhere in the text. 'link' has the same format as explain for the ``.. trac::`` directive below.
+
+``In the middle of my text `WikiFormatting`:trac: see!!!!`` 
+   In the middle of my text `WikiFormatting`:trac: see!!!!
+
+or
+
+``In the middle of my text :trac:`WikiFormatting` see!!!!`` 
+   In the middle of my text :trac:`WikiFormatting` see!!!!
+
+
+wiki
+====
+``.. trac:: WikiFormatting``
+	.. trac:: WikiFormatting
+
+``.. trac:: wiki:WikiFormatting``
+       .. trac:: wiki:WikiFormatting
+
+``.. trac:: wiki:WikiFormatting WikiFormatting``
+	.. trac:: wiki:WikiFormatting WikiFormatting
+
+``.. trac:: wiki:WikiFormatting LinkText``
+	.. trac:: wiki:WikiFormatting LinkText
+
+tickets
+=======
+
+``.. trac:: #1``
+	.. trac:: #1
+``.. trac:: #1 ticket one``
+	.. trac:: #1 ticket one
+``.. trac:: ticket:1``
+	.. trac:: ticket:1
+``.. trac:: ticket:1 ticket one``
+	.. trac:: ticket:1 ticket one
+
+reports
+=======
+
+``.. trac:: {1}``
+	.. trac:: {1}
+``.. trac:: {1} report one``
+        .. trac:: {1} report one
+``.. trac:: report:1``
+	.. trac:: report:1
+``.. trac:: report:1 report one``
+	.. trac:: report:1 report one
+
+changesets
+==========
+
+``.. trac:: [42]``
+	.. trac:: [42]
+``.. trac:: [42] changeset 42``
+	.. trac:: [42] changeset 42
+``.. trac:: changeset:42``
+	.. trac:: changeset:42
+``.. trac:: changeset:42 changeset 42``
+	.. trac:: changeset:42 changeset 42
+``.. trac:: foo``
+	.. trac:: foo
+
+files
+=====
+
+``.. trac:: browser:/trunk/trac``
+	.. trac:: browser:/trunk/trac
+
+The leading ``/`` can be omitted...
+
+``.. trac:: repos:trunk/trac trunk/trac``
+	.. trac:: repos:trunk/trac trunk/trac
+``.. trac:: source:trunk/trac Trac source code``
+	.. trac:: source:trunk/trac Trac source code
+
+``.. trac:: browser:trunk/README``
+	.. trac:: browser:trunk/README
+``.. trac:: repos:trunk/README trunk/README``
+	.. trac:: repos:trunk/README trunk/README
+``.. trac:: source:trunk/README README in trunk``
+	.. trac:: source:trunk/README README in trunk
+
+Note that if ``hoo`` is a file, the link targets its revision log. In order to see the file's content, you need to specify the revision explicitely, like here:
+
+``.. trac:: browser:/trunk/README#latest latest of trunk/README``
+	.. trac:: browser:/trunk/README#latest latest of trunk/README
+``.. trac:: repos:trunk/README#42 trunk/README in rev 42``
+	.. trac:: repos:trunk/README#42 trunk/README in rev 42
+}}}
+
+----
+See also: WikiRestructuredTextLinks, TracLinks
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-default/WikiStart
@@ -0,0 +1,43 @@
+= Welcome to Trac 0.10 =
+
+Trac is a '''minimalistic''' approach to '''web-based''' management of
+'''software projects'''. Its goal is to simplify effective tracking and handling of software issues, enhancements and overall progress.
+
+All aspects of Trac have been designed with the single goal to 
+'''help developers write great software''' while '''staying out of the way'''
+and imposing as little as possible on a team's established process and
+culture.
+
+As all Wiki pages, this page is editable, this means that you can
+modify the contents of this page simply by using your
+web-browser. Simply click on the "Edit this page" link at the bottom
+of the page. WikiFormatting will give you a detailed description of
+available Wiki formatting commands.
+
+"[wiki:TracAdmin trac-admin] ''yourenvdir'' initenv" created
+a new Trac environment, containing a default set of wiki pages and some sample
+data. This newly created environment also contains 
+[wiki:TracGuide documentation] to help you get started with your project.
+
+You can use [wiki:TracAdmin trac-admin] to configure
+[http://trac.edgewall.com/ Trac] to better fit your project, especially in
+regard to ''components'', ''versions'' and ''milestones''. 
+
+
+TracGuide is a good place to start.
+
+Enjoy! [[BR]]
+''The Trac Team''
+
+== Starting Points ==
+
+ * TracGuide --  Built-in Documentation
+ * [http://projects.edgewall.com/trac/ The Trac project] -- Trac Open Source Project
+ * [http://projects.edgewall.com/trac/wiki/TracFaq Trac FAQ] -- Frequently Asked Questions
+ * TracSupport --  Trac Support
+
+For a complete list of local wiki pages, see TitleIndex.
+
+Trac is brought to you by [http://www.edgewall.com/ Edgewall Software],
+providing professional Linux and software development services to clients
+worldwide. Visit http://www.edgewall.com/ for more information.
new file mode 100755
--- /dev/null
+++ b/examples/trac/wiki-default/checkwiki.py
@@ -0,0 +1,131 @@
+#!/usr/bin/python
+#
+# Check/update default wiki pages from the Trac project website.
+#
+# Note: This is a development tool used in Trac packaging/QA, not something
+#       particularly useful for end-users.
+#
+# Author: Daniel Lundin <daniel@edgewall.com>
+
+import httplib
+import re
+import sys
+import getopt
+
+# Pages to include in distribution
+wiki_pages = [
+ "CamelCase",
+ "InterMapTxt",
+ "InterTrac",
+ "InterWiki",
+ "RecentChanges",
+ "TitleIndex",
+ "TracAccessibility",
+ "TracAdmin",
+ "TracBackup",
+ "TracBrowser",
+ "TracCgi",
+ "TracChangeset",
+ "TracEnvironment",
+ "TracFastCgi",
+ "TracGuide",
+ "TracImport",
+ "TracIni",
+ "TracInstall",
+ "TracInterfaceCustomization",
+ "TracLinks",
+ "TracLogging",
+ "TracModPython",
+ "TracNotification",
+ "TracPermissions",
+ "TracPlugins",
+ "TracQuery",
+ "TracReports",
+ "TracRevisionLog",
+ "TracRoadmap",
+ "TracRss",
+ "TracSearch",
+ "TracStandalone",
+ "TracSupport",
+ "TracSyntaxColoring",
+ "TracTickets",
+ "TracTicketsCustomFields",
+ "TracTimeline",
+ "TracUnicode",
+ "TracUpgrade",
+ "TracWiki",
+ "WikiDeletePage",
+ "WikiFormatting",
+ "WikiHtml",
+ "WikiMacros",
+ "WikiNewPage",
+ "WikiPageNames",
+ "WikiProcessors",
+ "WikiRestructuredText",
+ "WikiRestructuredTextLinks"
+ ]
+
+def get_page_from_file (pname):
+    d = ''
+    try:
+        f = open(pname ,'r')
+        d = f.read()
+        f.close()
+    except:
+        print "Missing page: %s" % pname
+    return d
+
+def get_page_from_web (pname):
+    host = "projects.edgewall.com"
+    rfile = "/trac/wiki/%s?format=txt" % pname
+    c = httplib.HTTPConnection(host)
+    c.request("GET", rfile)
+    r = c.getresponse()
+    d = r.read()
+    if r.status != 200 or d == ("describe %s here\n" % pname):
+        c.close()
+        print "Missing page: %s" % pname
+    c.close()
+    f = open(pname, 'w+')
+    f.write(d)
+    f.close()
+    return d
+
+def check_links (data):
+    def get_refs(t, refs=[]):
+        r = "(?P<wikilink>(^|(?<=[^A-Za-z]))[!]?[A-Z][a-z/]+(?:[A-Z][a-z/]+)+)"
+        m = re.search (r, t)
+        if not m:
+            refs.sort()
+            result = []
+            orf = None
+            for rf in refs:
+                if rf != orf:
+                    result.append(rf)
+                    orf = rf
+            return result
+        refs.append(m.group())
+        return get_refs( t[m.end():], refs)
+    for p in data.keys():
+        links = get_refs(data[p], [])
+        for l in links:
+            if l not in data.keys():
+                print "Broken link:  %s -> %s" % (p, l)
+
+if __name__ == '__main__':
+    try:
+        opts, args = getopt.getopt(sys.argv[1:], "ds")
+    except getopt.GetoptError:
+        # print help information and exit:
+        print "%s [-d]" % sys.argv[0]
+        print "\t-d  -- Download pages from the main project wiki."
+        sys.exit()
+    get_page = get_page_from_file
+    for o,a in opts:
+        if o == '-d':
+            get_page = get_page_from_web
+    data = {}
+    for p in wiki_pages:
+        data[p] = get_page (p)
+    check_links(data)
+
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-macros/HelloWorld.py
@@ -0,0 +1,15 @@
+"""Example macro."""
+from trac.util import escape
+
+def execute(hdf, txt, env):
+    # Currently hdf is set only when the macro is called
+    # From a wiki page
+    if hdf:
+        hdf['wiki.macro.greeting'] = 'Hello World'
+        
+    # args will be `None` if the macro is called without parenthesis.
+    args = txt or 'No arguments'
+
+    # then, as `txt` comes from the user, it's important to guard against
+    # the possibility to inject malicious HTML/Javascript, by using `escape()`:
+    return 'Hello World, args = ' + escape(args)
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-macros/Timestamp.py
@@ -0,0 +1,6 @@
+"""Inserts the current time (in seconds) into the wiki page."""
+
+import time
+def execute(hdf, txt, env):
+    t = time.localtime()
+    return "<b>%s</b>" % time.strftime('%c', t)
new file mode 100644
--- /dev/null
+++ b/examples/trac/wiki-macros/TracGuideToc.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+"""
+This macro shows a quick and dirty way to make a table-of-contents for a set
+of wiki pages.
+"""
+
+TOC = [('TracGuide',                    'Index'),
+       ('TracInstall',                  'Installation'),
+       ('TracUpgrade',                  'Upgrading'),
+       ('TracIni',                      'Configuration'),
+       ('TracAdmin',                    'Administration'),
+       ('TracBackup',                   'Backup'),
+       ('TracLogging',                  'Logging'),
+       ('TracPermissions' ,             'Permissions'),
+       ('TracWiki',                     'The Wiki'),
+       ('WikiFormatting',               'Wiki Formatting'),
+       ('TracTimeline',                 'Timeline'),
+       ('TracBrowser',                  'Repository Browser'),
+       ('TracChangeset',                'Changesets'),
+       ('TracRoadmap',                  'Roadmap'),
+       ('TracTickets',                  'Tickets'),
+       ('TracQuery',                    'Ticket Queries'),
+       ('TracReports',                  'Reports'),
+       ('TracRss',                      'RSS Support'),
+       ('TracNotification',             'Notification'),
+       ('TracInterfaceCustomization',   'Customization'),
+       ('TracPlugins',                  'Plugins'),
+       ]
+
+def execute(hdf, args, env):
+    html = '<div class="wiki-toc">' \
+           '<h4>Table of Contents</h4>' \
+           '<ul>'
+    curpage = '%s' % hdf.getValue('wiki.page_name', '')
+    lang, page = '/' in curpage and curpage.split('/', 1) or ('', curpage)
+    for ref, title in TOC:
+        if page == ref:
+            cls =  ' class="active"'
+        else:
+            cls = ''
+        html += '<li%s><a href="%s">%s</a></li>' \
+                % (cls, env.href.wiki(lang+ref), title)
+    return html + '</ul></div>'
Copyright (C) 2012-2017 Edgewall Software