diff --git a/CHANGES b/CHANGES
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,212 @@
+Version 0.6.1 released 25 Aug 2009
+
+Instructions for upgrading from 0.5.3:
+
+- If you were using a Haskell configuration file, you will
+  need to create a new configuration file. 'gitit --print-default-config'
+  will print a self-documenting default configuration file in the new
+  format, which you can modify.
+
+- If your wiki contains discuss pages of the form 'foo:discuss.page',
+  rename them to '@foo.page'.
+
+- Delete template.html and the static directory so that these will
+  be replaced by the newest versions when you run gitit. If you have
+  customized these, you should back them up first, then merge your
+  changes into the new versions after they are created. (Note that
+  template.html will be replaced by a templates/ directory.)
+
+Summary of main changes:
+
+* Added support for plugins -- dynamically loaded Haskell programs that
+  transform pages. See the haddock documentation for Gitit.Interface for
+  plugin documentation. The plugins directory contains several sample
+  plugins.
+
+* Gitit's configuration file is now a text file with key-value pairs,
+  rather than a Haskell file.  The default configuration file
+  (which can be printed using `gitit --print-default-config`
+  contains comments that document all of the options.
+
+* Pages may now be written in (limited dialects of) LaTeX or HTML,
+  as well as markdown and reStructuredText.  The default format
+  is determined by a configuration option, but can be overridden
+  on a per-page basis using metadata (see below).  The default
+  Front Page and Help page are created in the default format specified
+  by the configuration file. In addition, syntax help is now displayed
+  to the left of the editing box when a page is being edited.
+
+* Pages may be written in literate Haskell, using either bird
+  style with markdown or reStructuredText, or LaTeX style with
+  LaTeX.  Literate Haskell can be made the default or specified on
+  a per-page basis.
+
+* Gitit now exports a library, Network.Gitit, that makes it easy for
+  any happstack application to embed a gitit wiki.
+
+* Added optional atom feeds, for whole site (at /_feed)
+  and for individual pages (at /_feed/path/to/page).
+  Feeds are cached with a configurable expiration time.
+
+* Completely new caching system.  Caching is turned off by default and
+  can be enabled by a configuration option.  Complete pages are cached
+  on disk and expired when pages are revised through the web interface.
+  When pages are modified directly through a VCS, the cache must be
+  refreshed manually, either by pressing Ctrl-R while viewing a page,
+  or by sending an HTTP request to /_expire/path/to/page, or by using
+  the included program expireGititPath. The new system is much faster
+  than the old in-memory cache, because it avoids the considerable
+  overhead of filestore calls to get the current revision id.
+
+* To make whole-page caching possible, the user login/out box has been
+  made into an ajax request to /_user.  jQuery is now loaded on every
+  page.
+
+* Math is converted to MathML by default (using the texmath library),
+  and a javascript is linked in that renders it correctly in IE+mathplayer,
+  Firefox, and Opera.  The 'math' configuration setting can alternatively
+  be set to 'jsMath' (to use jsMath javascript, which is more portable
+  but ugly and slower) or 'raw' (plain LaTeX code).
+
+* Routing changes for better handling of web spiders.  Instead of
+  "/foo?history" we now have "/_history/foo"; instead of "/foo?edit"
+  we haev "/_edit/foo"; etc.  This makes it possible to exclude web
+  spiders from non-cached pages by excluding URLs that start with
+  '/_'.  A default robots.txt file is now provided.  Users need not
+  do anything special for this to be enabled.
+
+* The authentication system has been revised and made much more
+  flexible. In the configuration file, you can specify either
+  'form', 'http', or 'generic' as authentication-method. Form
+  authentication is the old form-based gitit authentication system. HTTP
+  authentication presupposes that the wiki pages are locked down under
+  HTTP authentication; the gitit user will be set to the username used
+  for HTTP authentication. Generic authentication takes the username
+  from the REMOTE_USER request header. When gitit is being used as a
+  library, one can specify a custom withUser filter (which determines
+  the logged in user and sets REMOTE_USER accordingly) and a custom
+  authHandler (including handlers for /_login, /_logout, and whatever
+  else is needed).
+
+* Security fix: Gitit did not verify that a change password request
+  is genuine when it receives the final POST. It has been changed to
+  re-verify the reset code, otherwise an attacker could simply steal
+  anyone's account by spoofing a POST request. (Thanks to Robin Green.)
+
+* template.html has now been replaced by a directory, templates/, with
+  separate templates for each component of a page.
+
+* Added /_reloadTemplates action that recompiles the templates. (By
+  default the templates are compiled only on startup.)
+
+* Gitit's form-based authentication now includes a "password reset"
+  email.  Slightly modified from a patch from Henry Laxen.
+
+* The naming scheme for discussion pages has changed: the discussion
+  page for foo is now @foo, not foo:discuss. Reason: Windows, and
+  thus darcs, does not like colons in filenames.
+
+* Improved logging, with configurable verbosity.
+
+* Major code reorganization and cleanup.  Gitit has been moved under
+  the Network namespace. The old WebT handlers are replaced by new ones
+  in ServerPartT. 'handle' has been removed; instead, we use happstack's
+  routing combinators. Configuration and filestores are now passed
+  around in a reader monad, in WikiState. (This also allows different
+  wikis to have different configurations.) Most handlers have been
+  simplified so that they no longer require Page and Params arguments.
+  A new function, 'withInput', is used to avoid the need to pass Params
+  between handlers.
+
+* The static handler now "falls back" to the cabal data directory if the
+  requested file is not in "static" (or staticDir).  So the user need
+  no longer have a copy of the standard gitit CSS, javascript, and
+  image files in "static" (unless these are to be overridden). This
+  should make updates easier.  By default only 'custom.css' and
+  'logo.png' are put in the user's static directory.
+
+* Similarly, the templates in "templates" "fall back" to defaults in
+  the cabal data directory.  By default only 'footer.st' is put in
+  the user's static directory.
+
+* Gitit State now includes a renderPage function. This is more flexible
+  than storing a page template, since the user may want to use a custom
+  page rendering function, even one not based on string templates.
+
+* Added Network.Gitit.ContentTransformer module (thanks to Anton van
+  Straaten). The ContentTransformer module replaces Gitit.Convert. It
+  defines a number of single-purpose combinators that can be combined to
+  yield various kinds of content conversions. These are used to define
+  showPage, preview, showHighlightedSource, and other handlers that used
+  to be defined in Gitit.hs.
+
+* Verify in delete POST requests that filetodelete parameter matches
+  page.
+
+* Fixed revert when called from diff pages. Revert now reverts to the
+  older of the two revisions being compared.
+
+* Revamped auto-merging: user must now verify an edited page after
+  a merge, even if there were no conflicts.
+
+* Fixed Content-Disposition header on export so that filenames have
+  proper extensions.
+
+* Updated for happstack-server-0.3.3.  Since this version of happstack
+  supports UTF-8, gitit's old manual decoding and encoding were removed.
+
+* Use fileServeStrict instead of fileServe.  Resolves Issue #57.
+
+* 'limit' is no longer used in search.  The way it worked before was
+  confusing, since it limited total matches (usually to just a few files)
+  rather than limiting the number of matches in each file.
+
+* rdgreen's cautious-file library is now used to write the gitit-users
+  file. This makes it less likely that the file will be corrupted on
+  a power outage or hardware failure.
+
+* Redirects set properly after account creation.  If users go from
+  the Login form to the Register form, they are no longer redirected
+  back to the Login form after creating an account.
+
+* indexPage now uses filestore's new 'directory' function. It shows one
+  directory at a time. Subdirectories link to further index pages. This
+  improves on the old javascript folding interface, which did not preserve
+  state.  (Thanks to Thomas Hartman for suggestions.)
+
+* URLs of the form /a/b/ are now equivalent to /_index/a/b.
+
+* Improvements and bug fixes to deleting. Deleting a non-page now works.
+  You get a nice informative message if you try to delete a nonexistent
+  page or file.
+
+* Page names containing "..", "?", or "*", and '_' at beginning are
+  disallowed. Page names may now contain periods.
+
+* The "Permanent link" link has been removed. It relied on the sha1
+  parameter always being set, but we've changed that for performance
+  reasons.
+
+* Gitit can now be proxied to a subdirectory path. Thanks to Henry Laxen
+  for the idea and patches. See README for instructions.
+
+* Performance improvements (mostly due to Gwern Branwen):  Pages can be
+  compressed (configurable); unneeded filestore calls removed; cache-control:
+  max-age used.
+
+* Moved sidebar to end of HTML to make things easier for screen readers.
+
+* Moved search box and go box to templates.
+
+* Yahoo YUI CSS framework is now used for better consistency across browsers.
+  CSS cleaned up.  Icons for page types removed.
+
+* Fixed handling of 'forUser' parameter in 'recent activity'.
+
+* Made default maxUploadSize 10 Mb.
+
+* Renamed AppState -> GititState.
+
 Version 0.5.3 released 1 Feb 2009
 
 * Fixed bug which caused jsMath not to load.
@@ -9,13 +218,20 @@
 Version 0.5.1 released 1 Feb 2009
 
 * Major code reorganization, making gitit more modular.
+
 * Gitit can now optionally be built using Happstack instead of HAppS
   (just use -fhappstack when cabal installing).
+
 * Fixed bug with directories that had the same names as pages.
+
 * Added code from HAppS-Extra to fix cookie parsing problems.
+
 * New command-line options for --port, --debug.
+
 * New debug feature prints the date, the raw request, and
   the processed request data to standard output on each request.
+
 * Files with ".page" extension can no longer be uploaded.
+
 * Apostrophes and quotation marks now allowed in page names.
 
diff --git a/Gitit.hs b/Gitit.hs
deleted file mode 100644
--- a/Gitit.hs
+++ /dev/null
@@ -1,723 +0,0 @@
-{-# LANGUAGE Rank2Types, FlexibleContexts #-}
-{-
-Copyright (C) 2008 John MacFarlane <jgm@berkeley.edu>
-
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
--}
-
-module Main where
-
-import Gitit.Server
-import Gitit.Util (orIfNull, consolidateHeads)
-import Gitit.Initialize (createStaticIfMissing, createRepoIfMissing)
-import Gitit.Framework
-import Gitit.Layout
-import Gitit.Convert
-import Gitit.Export (exportFormats)
-import System.IO.UTF8
-import System.IO (stderr)
-import Control.Exception (throwIO, catch, try)
-import Prelude hiding (writeFile, readFile, putStrLn, putStr, catch)
-import System.Directory
-import System.Time
-import Control.Concurrent
-import System.FilePath
-import Gitit.State
-import Gitit.Config (getConfigFromOpts)
-import Text.XHtml hiding ( (</>), dir, method, password, rev )
-import qualified Text.XHtml as X ( password, method )
-import Data.List (intersperse, sort, nub, sortBy, isSuffixOf, find, isPrefixOf)
-import Data.Maybe (fromMaybe, fromJust, mapMaybe, isNothing)
-import Codec.Binary.UTF8.String (encodeString)
-import qualified Data.Map as M
-import Data.Ord (comparing)
-import Paths_gitit
-import Text.Pandoc
-import Text.Pandoc.Shared (substitute)
-import Data.Char (isAlphaNum, isAlpha, toLower)
-import Control.Monad.Reader
-import qualified Data.ByteString.Lazy as B
-import Network.HTTP (urlEncodeVars)
-import Text.Highlighting.Kate
-import qualified Text.StringTemplate as T
-import Data.DateTime (getCurrentTime, addMinutes, formatDateTime)
-import Network.Captcha.ReCaptcha (captchaFields, validateCaptcha)
-import Data.FileStore
-
-
-main :: IO ()
-main = do
-
-  -- parse options to get config file
-  conf <- getConfigFromOpts
-
-  -- check for external programs that are needed
-  let prereqs = "grep" : case repository conf of
-                      Git _        -> ["git"]
-                      Darcs _      -> ["darcs"]
-  forM_ prereqs $ \prog ->
-    findExecutable prog >>= \mbFind ->
-    when (isNothing mbFind) $ error $
-      "Required program '" ++ prog ++ "' not found in system path."
-
-  -- read user file and update state
-  userFileExists <- doesFileExist $ userFile conf
-  users' <- if userFileExists
-               then liftM (M.fromList . read) $ readFile $ userFile conf
-               else return M.empty
-
-  -- create template file if it doesn't exist
-  let templatefile = templateFile conf
-  templateExists <- doesFileExist templatefile
-  unless templateExists $ do
-    templatePath <- getDataFileName $ "data" </> "template.html"
-    copyFile templatePath templatefile
-    hPutStrLn stderr $ "Created " ++ templatefile
-
-  -- read template file
-  templ <- liftM T.newSTMP $ liftIO $ readFile templatefile
-
-  -- initialize state
-  initializeAppState conf users' templ
-
-  -- setup the page repository and static files, if they don't exist
-  createRepoIfMissing conf
-  let staticdir = staticDir conf
-  createStaticIfMissing staticdir
-
-  -- start the server
-  hPutStrLn stderr $ "Starting server on port " ++ show (portNumber conf)
-  tid <- forkIO $ simpleHTTP (Conf { validator = Nothing, port = portNumber conf }) $
-          map (\d -> dir d [ withExpiresHeaders $ fileServe [] $ staticdir </> d]) ["css", "img", "js"] ++
-          [ debugHandler | debugMode conf ] ++
-          [ filterIf acceptsZip gzipBinary $ cookieFixer $ multi wikiHandlers ]
-  waitForTermination
-
-  -- shut down the server
-  putStrLn "Shutting down..."
-  killThread tid
-  putStrLn "Shutdown complete"
-
-wikiHandlers :: [Handler]
-wikiHandlers = [ handlePath "_index"     GET  indexPage
-               , handlePath "_activity"  GET  showActivity
-               , handlePath "_preview"   POST preview
-               , handlePath "_go"        POST goToPage
-               , handlePath "_search"    POST searchResults
-               , handlePath "_search"    GET  searchResults
-               , handlePath "_register"  GET  registerUserForm
-               , handlePath "_register"  POST registerUser
-               , handlePath "_login"     GET  loginUserForm
-               , handlePath "_login"     POST loginUser
-               , handlePath "_logout"    GET  logoutUser
-               , handlePath "_upload"    GET  (ifLoggedIn uploadForm loginUserForm)
-               , handlePath "_upload"    POST (ifLoggedIn uploadFile loginUserForm)
-               , handlePath "_random"    GET  randomPage
-               , handlePath ""           GET  showFrontPage
-               , withCommand "showraw" [ handlePage GET showRawPage ]
-               , withCommand "history" [ handlePage GET showPageHistory,
-                                         handle (not . isPage) GET showFileHistory ]
-               , withCommand "edit"    [ handlePage GET $ unlessNoEdit (ifLoggedIn editPage loginUserForm) showPage ]
-               , withCommand "diff"    [ handlePage GET  showPageDiff,
-                                         handle isSourceCode GET showFileDiff ]
-               , withCommand "export"  [ handlePage POST exportPage, handlePage GET exportPage ]
-               , withCommand "cancel"  [ handlePage POST showPage ]
-               , withCommand "discuss" [ handlePage GET discussPage ]
-               , withCommand "update"  [ handlePage POST $ unlessNoEdit (ifLoggedIn updatePage loginUserForm) showPage ]
-               , withCommand "delete"  [ handlePage GET  $ unlessNoDelete (ifLoggedIn confirmDelete loginUserForm) showPage,
-                                         handlePage POST $ unlessNoDelete (ifLoggedIn deletePage loginUserForm) showPage ]
-               , handlePage GET showPage
-               , handleSourceCode
-               , handleAny
-               , handlePage GET createPage
-               ]
-
-handleSourceCode :: Handler
-handleSourceCode = withData $ \com ->
-  case com of
-       Command (Just "showraw") -> [ handle isSourceCode GET showFileAsText ]
-       _                        -> [ handle isSourceCode GET showHighlightedSource ]
-
-handleAny :: Handler
-handleAny = 
-  uriRest $ \uri -> let path' = uriPath uri
-                    in  do fs <- getFileStore
-                           mimetype <- getMimeTypeForExtension (takeExtension path')
-                           res <- liftIO $ try $ (retrieve fs path' Nothing  :: IO B.ByteString)
-                           case res of
-                                  Right contents -> anyRequest $ ok $ setContentType mimetype $
-                                                               (toResponse noHtml) {rsBody = contents} -- ugly hack
-                                  Left NotFound  -> anyRequest noHandle
-                                  Left e         -> error (show e)
-
-debugHandler :: Handler
-debugHandler = do
-  liftIO $ putStr "\n"
-  withRequest $ \req -> liftIO $ getCurrentTime >>= (putStrLn . formatDateTime "%c") >> putStrLn (show req)
-  multi [ handle (const True) GET showParams, handle (const True) POST showParams ]
-    where showParams page params = do
-            liftIO $ putStrLn page >> putStrLn (show params)
-            noHandle
-
-showRawPage :: String -> Params -> Web Response
-showRawPage = showFileAsText . pathForPage
-
-showFileAsText :: String -> Params -> Web Response
-showFileAsText file params = do
-  mContents <- rawContents file params
-  case mContents of
-       Nothing   -> error "Unable to retrieve page contents."
-       Just c    -> ok $ setContentType "text/plain; charset=utf-8" $ toResponse $ encodeString c
-
-randomPage :: String -> Params -> Web Response
-randomPage _ _ = do
-  fs <- getFileStore
-  files <- liftIO $ index fs
-  let pages = map dropExtension $ filter (\f -> takeExtension f == ".page" && not (":discuss.page" `isSuffixOf` f)) files
-  if null pages
-     then error "No pages found!"
-     else do
-       TOD _ picosecs <- liftIO getClockTime
-       let newPage = pages !! ((fromIntegral picosecs `div` 1000000) `mod` length pages)
-       seeOther (urlForPage newPage) $ toResponse $ p << "Redirecting to a random page"
-
-showFrontPage :: String -> Params -> Web Response
-showFrontPage _ params = do
-  cfg <- getConfig
-  showPage (frontPage cfg) params
-
-showPage :: String -> Params -> Web Response
-showPage page params = do
-  jsMathExists <- queryAppState jsMath
-  mbCached <- lookupCache (pathForPage page) (pRevision params)
-  case mbCached of
-         Just cp ->
-           formattedPage (defaultPageLayout { pgScripts = ["jsMath/easy/load.js" | jsMathExists]}) page params $ cpContents cp
-         _ -> do
-           mDoc <- pageAsPandoc page params
-           case mDoc of
-                Just d  -> do
-                  let divify c = thediv ! [identifier "wikipage",
-                                            strAttr "onDblClick" ("window.location = '" ++ urlForPage page ++
-                                            "?edit" ++
-                                            (case (pRevision params) of
-                                                  Nothing -> ""
-                                                  Just r  -> urlEncodeVars [("revision", r),("logMsg", "Revert to " ++ r)]) ++ "';")] << c
-
-                  c <- liftM divify $ pandocToHtml d
-                  when (isNothing (pRevision params)) $ do
-                    -- TODO not quite ideal, since page might have been modified after being retrieved by pageAsPandoc
-                    -- better to have pageAsPandoc return the revision ID too...
-                    fs <- getFileStore
-                    rev <- liftIO $ latest fs (pathForPage page)
-                    cacheContents (pathForPage page) rev c
-                  formattedPage (defaultPageLayout { pgScripts = ["jsMath/easy/load.js" | jsMathExists]}) page params c
-                Nothing -> noHandle
-
-discussPage :: String -> Params -> Web Response
-discussPage page params = do
-  if isDiscussPage page
-     then showPage page params
-     else showPage (page ++ ":discuss") params
-
-createPage :: String -> Params -> Web Response
-createPage page params =
-  formattedPage (defaultPageLayout { pgTabs = [] }) page params $
-     p << [ stringToHtml ("There is no page '" ++ page ++ "'.  You may create the page by ")
-          , anchor ! [href $ urlForPage page ++ "?edit"] << "clicking here." ] 
-
-uploadForm :: String -> Params -> Web Response
-uploadForm _ params = do
-  let page = "_upload"
-  let origPath = pFilename params
-  let wikiname = pWikiname params `orIfNull` takeFileName origPath
-  let logMsg = pLogMsg params
-  let upForm = form ! [X.method "post", enctype "multipart/form-data"] << fieldset <<
-        [ p << [label << "File to upload:", br, afile "file" ! [value origPath] ]
-        , p << [label << "Name on wiki, including extension",
-                noscript << " (leave blank to use the same filename)", stringToHtml ":", br,
-                textfield "wikiname" ! [value wikiname],
-                primHtmlChar "nbsp", checkbox "overwrite" "yes", label << "Overwrite existing file"]
-        , p << [label << "Description of content or changes:", br, textfield "logMsg" ! [size "60", value logMsg],
-                submit "upload" "Upload"] ]
-  formattedPage (defaultPageLayout { pgScripts = ["uploadForm.js"], pgShowPageTools = False, pgTabs = [], pgTitle = "Upload a file"} ) page params upForm
-
-uploadFile :: String -> Params -> Web Response
-uploadFile _ params = do
-  let page = "_upload"
-  let origPath = pFilename params
-  let fileContents = pFileContents params
-  let wikiname = pWikiname params `orIfNull` takeFileName origPath
-  let logMsg = pLogMsg params
-  cfg <- getConfig
-  mbUser <- getUser $ pUser params
-  (user, email) <- case mbUser of
-                        Nothing -> fail "User must be logged in to delete page."
-                        Just u  -> return (uUsername u, uEmail u)
-  let overwrite = pOverwrite params
-  fs <- getFileStore
-  exists <- liftIO $ catch (latest fs wikiname >> return True) (\e -> if e == NotFound then return False else throwIO e >> return True)
-  let imageExtensions = [".png", ".jpg", ".gif"]
-  let errors = validate [ (null logMsg, "Description cannot be empty.")
-                        , (null origPath, "File not found.")
-                        , (not overwrite && exists, "A file named '" ++ wikiname ++
-                           "' already exists in the repository: choose a new name " ++
-                           "or check the box to overwrite the existing file existing file.")
-                        , (B.length fileContents > fromIntegral (maxUploadSize cfg),
-                           "File exceeds maximum upload size.")
-                        , (takeExtension wikiname == ".page",
-                           "This file extension is reserved for wiki pages.")
-                        ]
-  if null errors
-     then do
-       liftIO $ save fs wikiname (Author user email) logMsg fileContents
-       formattedPage (defaultPageLayout { pgShowPageTools = False, pgTabs = [], pgTitle = "Upload successful" }) page params $
-                     thediv << [ h2 << ("Uploaded " ++ show (B.length fileContents) ++ " bytes")
-                               , if takeExtension wikiname `elem` imageExtensions
-                                    then p << "To add this image to a page, use:" +++
-                                         pre << ("![alt text](/" ++ wikiname ++ ")")
-                                    else p << "To link to this resource from a page, use:" +++
-                                         pre << ("[link label](/" ++ wikiname ++ ")") ]
-     else uploadForm page (params { pMessages = errors })
-
-goToPage :: String -> Params -> Web Response
-goToPage _ params = do
-  let gotopage = pGotoPage params
-  fs <- getFileStore
-  allPageNames <- liftM (map dropExtension . filter (".page" `isSuffixOf`)) $ liftIO $ index fs
-  let findPage f = find f allPageNames
-  case findPage (gotopage ==) of
-       Just m  -> seeOther (urlForPage m) $ toResponse "Redirecting to exact match"
-       Nothing -> case findPage (\n -> (map toLower gotopage) == (map toLower n)) of
-                       Just m  -> seeOther (urlForPage m) $ toResponse "Redirecting to case-insensitive match"
-                       Nothing -> case findPage (\n -> (map toLower gotopage) `isPrefixOf` (map toLower n)) of
-                                       Just m  -> seeOther (urlForPage m) $ toResponse "Redirecting to partial match"
-                                       Nothing -> searchResults "" params{ pPatterns = words gotopage }
-
-searchResults :: String -> Params -> Web Response
-searchResults _ params = do
-  let page = "_search"
-  let patterns = pPatterns params
-  let limit = pLimit params
-  fs <- getFileStore
-  matchLines <- if null patterns
-                   then return []
-                   else liftM (take limit) $ liftIO $ search fs defaultSearchQuery{queryPatterns = patterns}
-  let contentMatches = map matchResourceName matchLines
-  allPages <- liftM (filter (".page" `isSuffixOf`)) $ liftIO $ index fs
-  let matchesPatterns pageName = all (`elem` (words $ map toLower $ dropExtension pageName)) $ map (map toLower) patterns
-  let pageNameMatches = filter matchesPatterns allPages
-  let allMatchedFiles = nub $ filter (".page" `isSuffixOf`) contentMatches ++ pageNameMatches
-  let matches = map (\f -> (f, mapMaybe (\x -> if matchResourceName x == f then Just (matchLine x) else Nothing) matchLines)) allMatchedFiles
-  let relevance (f, ms) = length ms + if f `elem` pageNameMatches then 100 else 0
-  let preamble = if null matches
-                    then h3 << if null patterns
-                                  then ["Please enter a search term."]
-                                  else ["No matches found for '", unwords patterns, "':"]
-                    else h3 << [(show $ length matches), " matches found for '", unwords patterns, "':"]
-  let htmlMatches = preamble +++ olist << map
-                      (\(file, contents) -> li << [anchor ! [href $ urlForPage $ takeBaseName file] << takeBaseName file,
-                      stringToHtml (" (" ++ show (length contents) ++ " matching lines)"),
-                      stringToHtml " ", anchor ! [href "#", theclass "showmatch", thestyle "display: none;"] <<
-                      if length contents > 0 then "[show matches]" else "",
-                      pre ! [theclass "matches"] << unlines contents])
-                      (reverse $ sortBy (comparing relevance) matches)
-  formattedPage (defaultPageLayout { pgShowPageTools = False, pgTabs = [], pgScripts = ["search.js"], pgTitle = "Search results"}) page params htmlMatches
-
-preview :: String -> Params -> Web Response
-preview _ params = do
-  pt <- getDefaultPageType -- should get the current page type instead
-  pandocToHtml (textToPandoc pt $ pRaw params) >>= ok . toResponse . encodeString . renderHtmlFragment
-
-showPageHistory :: String -> Params -> Web Response
-showPageHistory page params = showHistory (pathForPage page) page params
-
-showFileHistory :: String -> Params -> Web Response
-showFileHistory file params = showHistory file file params
-
-showHistory :: String -> String -> Params -> Web Response
-showHistory file page params =  do
-  currTime <- liftIO getCurrentTime
-  let oneYearAgo = addMinutes (-1 * 60 * 24 * 365) currTime
-  let since = case pSince params of
-                   Nothing -> Just oneYearAgo
-                   Just t  -> Just t
-  fs <- getFileStore
-  hist <- liftIO $ history fs [file] (TimeRange since Nothing)
-  if null hist
-     then noHandle
-     else do
-       let versionToHtml rev pos = 
-              li ! [theclass "difflink", intAttr "order" pos, strAttr "revision" $ revId rev] <<
-                   [thespan ! [theclass "date"] << (show $ revDateTime rev), stringToHtml " (",
-                    thespan ! [theclass "author"] <<
-                            anchor ! [href $ "/_activity?" ++ urlEncodeVars [("forUser", authorName $ revAuthor rev)]] <<
-                                       (authorName $ revAuthor rev), stringToHtml ")", stringToHtml ": ",
-                    anchor ! [href (urlForPage page ++ "?revision=" ++ revId rev)] <<
-                    thespan ! [theclass "subject"] <<  revDescription rev,
-                    noscript << ([stringToHtml " [compare with ",
-                    anchor ! [href $ urlForPage page ++ "?diff&from=" ++ revId rev ++
-                              "^&to=" ++ revId rev] << "previous"] ++
-                                 (if pos /= 1
-                                     then [primHtmlChar "nbsp", primHtmlChar "bull",
-                                           primHtmlChar "nbsp",
-                                           anchor ! [href $ urlForPage page ++ "?diff&from=" ++
-                                                     revId rev ++ "&to=HEAD"] << "current" ]
-                                     else []) ++
-                                 [stringToHtml "]"])]
-       let contents = ulist ! [theclass "history"] << zipWith versionToHtml hist [(length hist), (length hist - 1)..1]
-       formattedPage (defaultPageLayout { pgScripts = ["dragdiff.js"], pgSelectedTab = HistoryTab, pgTitle = ("Changes to " ++ page) }) page params contents
-
-showActivity :: String -> Params -> Web Response
-showActivity _ params = do
-  let page = "_activity"
-  currTime <- liftIO getCurrentTime
-  let oneMonthAgo = addMinutes (-1 * 60 * 24 * 30) currTime
-  let since = case pSince params of
-                   Nothing -> Just oneMonthAgo
-                   Just t  -> Just t
-  let forUser = pForUser params
-  fs <- getFileStore
-  hist <- liftIO $ history fs [] (TimeRange since Nothing)
-  let fileFromChange (Added f) = f
-      fileFromChange (Modified f) = f
-      fileFromChange (Deleted f) = f
-  let filesFor changes revis = intersperse (primHtmlChar "nbsp") $ map
-                             (\file -> anchor ! [href $ urlForPage file ++ "?diff&to=" ++ revis] << file) $ map
-                             (\file -> if ".page" `isSuffixOf` file then dropExtension file else file) $ map fileFromChange changes 
-  let heading = h1 << ("Recent changes" ++ if null forUser then "" else (" by " ++ forUser))
-  let contents = ulist ! [theclass "history"] << map (\rev -> li <<
-                           [thespan ! [theclass "date"] << (show $ revDateTime rev), stringToHtml " (",
-                            thespan ! [theclass "author"] <<
-                                    anchor ! [href $ "/_activity?" ++ urlEncodeVars [("forUser", authorName $ revAuthor rev)]] <<
-                                               (authorName $ revAuthor rev), stringToHtml "): ",
-                            thespan ! [theclass "subject"] << revDescription rev, stringToHtml " (",
-                            thespan ! [theclass "files"] << filesFor (revChanges rev) (revId rev),
-                            stringToHtml ")"]) hist
-  formattedPage (defaultPageLayout { pgShowPageTools = False, pgTabs = [], pgTitle = "Recent changes" }) page params (heading +++ contents)
-
-showPageDiff :: String -> Params -> Web Response
-showPageDiff page params = showDiff (pathForPage page) page params
-
-showFileDiff :: String -> Params -> Web Response
-showFileDiff page params = showDiff page page params
-
-showDiff :: String -> String -> Params -> Web Response
-showDiff file page params = do
-  let from = pFrom params
-  let to = pTo params
-  fs <- getFileStore
-  from' <- case from of
-              Nothing -> do
-                pageHist <- liftIO $ history fs [pathForPage page] (TimeRange Nothing Nothing)
-                if length pageHist < 2
-                   then return Nothing
-                   else case to of
-                            Nothing -> return Nothing
-                            Just t  -> let (_, upto) = break (\r -> idsMatch fs (revId r) t) pageHist
-                                       in  return $
-                                           if length upto >= 2
-                                              then Just $ revId $ upto !! 1  -- the immediately preceding revision
-                                              else Nothing
-              x       -> return x
-  rawDiff <- liftIO $ diff fs file from' to
-  let diffItemToHtml (B, xs) = thespan << xs
-      diffItemToHtml (F, xs) = thespan ! [theclass "deleted"] << xs
-      diffItemToHtml (S, xs) = thespan ! [theclass "added"]   << xs
-  let formattedDiff = h2 ! [theclass "revision"] << ("Changes from " ++ case from' of { Just r -> r; Nothing -> "beginning" }) +++
-                      pre ! [theclass "diff"] << map diffItemToHtml rawDiff
-  formattedPage (defaultPageLayout { pgTabs = DiffTab : pgTabs defaultPageLayout, pgSelectedTab = DiffTab })
-                page (params { pRevision = to }) formattedDiff
-
-editPage :: String -> Params -> Web Response
-editPage page params = do
-  let rev = pRevision params
-  let messages = pMessages params
-  fs <- getFileStore
-  (mbRev, raw) <- case pEditedText params of
-                       Nothing -> liftIO $ catch
-                                          (do c <- liftIO $ retrieve fs (pathForPage page) rev
-                                              r <- liftIO $ case rev of
-                                                                 Nothing  -> latest fs (pathForPage page) >>= revision fs
-                                                                 Just r   -> revision fs r
-                                              return $ (Just $ revId r, c))
-                                          (\e -> if e == NotFound
-                                                    then return (Nothing, "")
-                                                    else throwIO e)
-                       Just t -> return (if null (pSHA1 params) then Nothing else Just (pSHA1 params), t)
-  let logMsg = pLogMsg params
-  let sha1Box = case mbRev of
-                 Just r  -> textfield "sha1" ! [thestyle "display: none", value r]
-                 Nothing -> noHtml
-  let editForm = gui (urlForPage page) ! [identifier "editform"] <<
-                   [sha1Box,
-                    textarea ! [cols "80", name "editedText", identifier "editedText"] << raw, br,
-                    label << "Description of changes:", br,
-                    textfield "logMsg" ! [value logMsg],
-                    submit "update" "Save", primHtmlChar "nbsp",
-                    submit "cancel" "Discard", primHtmlChar "nbsp",
-                    input ! [thetype "button", theclass "editButton", identifier "previewButton",
-                             strAttr "onClick" "updatePreviewPane();", strAttr "style" "display: none;", value "Preview" ],
-                    thediv ! [ identifier "previewpane" ] << noHtml ]
-  formattedPage (defaultPageLayout { pgShowPageTools = False, pgSelectedTab = EditTab,
-                                     pgScripts = ["preview.js"], pgTitle = ("Editing " ++ page) }) page (params {pMessages = messages}) editForm
-
-confirmDelete :: String -> Params -> Web Response
-confirmDelete page params = do
-  let confirmForm = gui "" <<
-        [ p << "Are you sure you want to delete this page?"
-        , submit "confirm" "Yes, delete it!"
-        , stringToHtml " "
-        , submit "cancel" "No, keep it!"
-        , br ]
-  formattedPage defaultPageLayout page params confirmForm
-
-deletePage :: String -> Params -> Web Response
-deletePage page params = do
-  mbUser <- getUser $ pUser params
-  (user, email) <- case mbUser of
-                        Nothing -> fail "User must be logged in to delete page."
-                        Just u  -> return (uUsername u, uEmail u)
-  if pConfirm params
-     then do
-       fs <- getFileStore
-       liftIO $ delete fs (pathForPage page) (Author user email) "Deleted using web interface."
-       seeOther "/" $ toResponse $ p << "Page deleted"
-     else seeOther (urlForPage page) $ toResponse $ p << "Page not deleted"
-
-updatePage :: String -> Params -> Web Response
-updatePage page params = do
-  mbUser <- getUser $ pUser params
-  (user, email) <- case mbUser of
-                        Nothing -> fail "User must be logged in to delete page."
-                        Just u  -> return (uUsername u, uEmail u)
-  let editedText = case pEditedText params of
-                      Nothing -> error "No body text in POST request"
-                      Just b  -> b
-  let logMsg = pLogMsg params
-  let oldSHA1 = pSHA1 params
-  fs <- getFileStore
-  if null logMsg
-     then editPage page (params { pMessages = ["Description cannot be empty."] })
-     else do
-       cfg <- getConfig
-       if length editedText > fromIntegral (maxUploadSize cfg)
-          then error "Page exceeds maximum size."
-          else return ()
-       -- ensure that every file has a newline at the end, to avoid "No newline at eof" messages in diffs
-       let editedText' = if null editedText || last editedText == '\n' then editedText else editedText ++ "\n"
-       -- check SHA1 in case page has been modified, merge
-       modifyRes <-    if null oldSHA1
-                          then liftIO $ create fs (pathForPage page) (Author user email) logMsg editedText' >> return (Right ())
-                          else liftIO $ catch (modify fs (pathForPage page) oldSHA1 (Author user email) logMsg editedText')
-                                     (\e -> if e == Unchanged then return (Right ()) else throwIO e)
-       case modifyRes of
-            Right ()       -> seeOther (urlForPage page) $ toResponse $ p << "Page updated"
-            Left (MergeInfo mergedWithRev False mergedText) ->
-                              updatePage page params{ pMessages = ("Merged with revision " ++ revId mergedWithRev) : pMessages params,
-                                                      pEditedText = Just mergedText,
-                                                      pSHA1 = revId mergedWithRev }
-            Left (MergeInfo mergedWithRev True mergedText) -> do
-               let mergeMsg = "The page has been edited since you checked it out. " ++
-                              "Changes have been merged into your edits below. " ++
-                              "Please resolve conflicts and Save."
-               editPage page (params { pEditedText = Just mergedText
-                                     , pSHA1 = revId mergedWithRev
-                                     , pMessages = [mergeMsg] })
-
-indexPage :: String -> Params -> Web Response
-indexPage _ params = do
-  let page = "_index"
-  fs <- getFileStore
-  files <- liftIO $ index fs
-  let htmlIndex = fileListToHtml "/" $ map splitPath $ sort $ filter (\f -> not (":discuss.page" `isSuffixOf` f)) files
-  formattedPage (defaultPageLayout { pgShowPageTools = False, pgTabs = [], pgScripts = ["folding.js"], pgTitle = "All pages" }) page params htmlIndex
-
--- | Create a hierarchical ordered list (with links) for a list of files
-fileListToHtml :: String -> [[FilePath]] -> Html
-fileListToHtml prefix lst = ulist ! [identifier "index", theclass "folding"] <<
-  (map (\(h, l) -> let h' = if takeExtension h == ".page" then dropExtension h else h
-                   in if [] `elem` l
-                         then li ! [theclass $ if takeExtension h == ".page" then "page" else "upload"] << anchor ! [href $ prefix ++ h'] << h'
-                         else li ! [theclass "folder"] << [stringToHtml h', fileListToHtml (prefix ++ h') l]) $
-  consolidateHeads lst)
-
--- user authentication
-loginForm :: Html
-loginForm =
-  gui "/_login" ! [identifier "loginForm"] << fieldset <<
-     [ label << "Username ", textfield "username" ! [size "15"], stringToHtml " "
-     , label << "Password ", X.password "password" ! [size "15"], stringToHtml " "
-     , submit "login" "Login"] +++
-     p << [ stringToHtml "If you do not have an account, "
-          , anchor ! [href "/_register"] << "click here to get one." ]
-
-loginUserForm :: String -> Params -> Web Response
-loginUserForm page params =
-  addCookie (60 * 10) (mkCookie "destination" $ substitute " " "%20" $ fromMaybe "/" $ pReferer params) >>
-  loginUserForm' page params
-
-loginUserForm' :: String -> Params -> Web Response
-loginUserForm' page params =
-  formattedPage (defaultPageLayout { pgShowPageTools = False, pgTabs = [], pgTitle = "Login" }) page params loginForm
-
-loginUser :: String -> Params -> Web Response
-loginUser page params = do
-  let uname = pUsername params
-  let pword = pPassword params
-  let destination = pDestination params
-  allowed <- authUser uname pword
-  if allowed
-    then do
-      key <- newSession (SessionData uname)
-      addCookie sessionTime (mkCookie "sid" (show key))
-      addCookie 0 (mkCookie "destination" "/")   -- remove unneeded destination cookie
-      seeOther destination $ toResponse $ p << ("Welcome, " ++ uname)
-    else
-      loginUserForm' page (params { pMessages = "Authentication failed." : pMessages params })
-
-logoutUser :: String -> Params -> Web Response
-logoutUser _ params = do
-  let key = pSessionKey params
-  let destination = substitute " " "%20" $ fromMaybe "/" $ pReferer params
-  case key of
-       Just k  -> do
-         delSession k
-         addCookie 0 (mkCookie "sid" "-1")  -- make cookie expire immediately, effectively deleting it
-       Nothing -> return ()
-  seeOther destination $ toResponse "You have been logged out."
-
-registerForm :: Web Html
-registerForm = do
-  cfg <- getConfig
-  let accessQ = case accessQuestion cfg of
-                      Nothing          -> noHtml
-                      Just (prompt, _) -> label << prompt +++ br +++
-                                          X.password "accessCode" ! [size "15"] +++ br
-  let captcha = if useRecaptcha cfg
-                   then captchaFields (recaptchaPublicKey cfg) Nothing
-                   else noHtml
-  return $ gui "" ! [identifier "loginForm"] << fieldset <<
-            [ accessQ
-            , label << "Username (at least 3 letters or digits):", br
-            , textfield "username" ! [size "20"], stringToHtml " ", br
-            , label << "Email (optional, will not be displayed on the Wiki):", br
-            , textfield "email" ! [size "20"], br
-            , textfield "fullname" ! [size "20", theclass "req"]
-            , label << "Password (at least 6 characters, including at least one non-letter):", br
-            , X.password "password" ! [size "20"], stringToHtml " ", br
-            , label << "Confirm Password:", br, X.password "password2" ! [size "20"], stringToHtml " ", br
-            , captcha
-            , submit "register" "Register" ]
-
-registerUserForm :: String -> Params -> Web Response
-registerUserForm _ params =
-  addCookie (60 * 10) (mkCookie "destination" $ substitute " " "%20" $ fromMaybe "/" $ pReferer params) >>
-  registerForm >>=
-  formattedPage (defaultPageLayout { pgShowPageTools = False, pgTabs = [], pgTitle = "Register for an account" }) "_register" params
-
-registerUser :: String -> Params -> Web Response
-registerUser _ params = do
-  let isValidUsername u = length u >= 3 && all isAlphaNum u
-  let isValidPassword pw = length pw >= 6 && not (all isAlpha pw)
-  let accessCode = pAccessCode params
-  let uname = pUsername params
-  let pword = pPassword params
-  let pword2 = pPassword2 params
-  let email = pEmail params
-  let fakeField = pFullName params
-  let recaptcha = pRecaptcha params
-  taken <- isUser uname
-  cfg <- getConfig
-  let isValidAccessCode = case accessQuestion cfg of
-        Nothing           -> True
-        Just (_, answers) -> accessCode `elem` answers
-  let isValidEmail e = length (filter (=='@') e) == 1
-  captchaResult  <- if useRecaptcha cfg
-                       then if null (recaptchaChallengeField recaptcha) || null (recaptchaResponseField recaptcha)
-                               then return $ Left "missing-challenge-or-response"  -- no need to bother captcha.net in this case
-                               else liftIO $ do
-                                      mbIPaddr <- lookupIPAddr $ pPeer params
-                                      let ipaddr = case mbIPaddr of
-                                                        Just ip -> ip
-                                                        Nothing -> error $ "Could not find ip address for " ++ pPeer params
-                                      ipaddr `seq` validateCaptcha (recaptchaPrivateKey cfg) ipaddr (recaptchaChallengeField recaptcha)
-                                                        (recaptchaResponseField recaptcha)
-                       else return $ Right ()
-  let (validCaptcha, captchaError) = case captchaResult of
-                                      Right () -> (True, Nothing)
-                                      Left err -> (False, Just err)
-  let errors = validate [ (taken, "Sorry, that username is already taken.")
-                        , (not isValidAccessCode, "Incorrect response to access prompt.")
-                        , (not (isValidUsername uname), "Username must be at least 3 charcaters, all letters or digits.")
-                        , (not (isValidPassword pword), "Password must be at least 6 characters, with at least one non-letter.")
-                        , (not (null email) && not (isValidEmail email), "Email address appears invalid.")
-                        , (pword /= pword2, "Password does not match confirmation.")
-                        , (not validCaptcha, "Failed CAPTCHA (" ++ fromJust captchaError ++ "). Are you really human?")
-                        , (not (null fakeField), "You do not seem human enough.") ] -- fakeField is hidden in CSS (honeypot)
-  if null errors
-     then do
-       user <- liftIO $ mkUser uname email pword
-       addUser uname user
-       loginUser "/" (params { pUsername = uname, pPassword = pword, pEmail = email })
-     else registerForm >>=
-          formattedPage (defaultPageLayout { pgShowPageTools = False, pgTabs = [], pgTitle = "Register for an account" })
-                    "_register" (params { pMessages = errors })
-
-showHighlightedSource :: String -> Params -> Web Response
-showHighlightedSource file params = do
-  mbCached <- lookupCache file (pRevision params)
-  case mbCached of
-         Just cp -> formattedPage defaultPageLayout file params $ cpContents cp
-         _ -> do
-           contents <- rawContents file params
-           case contents of
-               Just source -> let lang' = head $ languagesByExtension $ takeExtension file
-                              in case highlightAs lang' (filter (/='\r') source) of
-                                       Left _       -> noHandle
-                                       Right res    -> do
-                                         let formattedContents = formatAsXHtml [OptNumberLines] lang' res
-                                         when (isNothing (pRevision params)) $ do
-                                           fs <- getFileStore
-                                           rev <- liftIO $ latest fs file
-                                           cacheContents file rev formattedContents
-                                         formattedPage defaultPageLayout file params $ formattedContents
-               Nothing     -> noHandle
-
-exportPage :: String -> Params -> Web Response
-exportPage page params = do
-  let format = pFormat params
-  mDoc <- pageAsPandoc page params
-  case mDoc of
-       Nothing  -> error $ "Unable to retrieve page contents."
-       Just doc -> case lookup format exportFormats of
-                        Nothing     -> error $ "Unknown export format: " ++ format
-                        Just writer -> writer page doc
-
-rawContents :: String -> Params -> Web (Maybe String)
-rawContents file params = do
-  let rev = pRevision params
-  fs <- getFileStore
-  liftIO $ catch (retrieve fs file rev >>= return . Just) (\e -> if e == NotFound then return Nothing else throwIO e)
-
-pageAsPandoc :: String -> Params -> Web (Maybe Pandoc)
-pageAsPandoc page params = do
-  pt <- getDefaultPageType
-  mDoc <- rawContents (pathForPage page) params >>= (return . liftM (textToPandoc pt))
-  return $ case mDoc of
-           Nothing                -> Nothing
-           Just (Pandoc _ blocks) -> Just $ Pandoc (Meta [Str page] [] []) blocks
-
-
diff --git a/Gitit/Config.hs b/Gitit/Config.hs
deleted file mode 100644
--- a/Gitit/Config.hs
+++ /dev/null
@@ -1,82 +0,0 @@
-{-# LANGUAGE CPP #-}
-{-
-Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
-
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
--}
-
-{- Functions for parsing command line options and reading the config file. 
--}
-
-module Gitit.Config ( getConfigFromOpts )
-where
-import Gitit.State (Config(..), defaultConfig)
-import System.Environment
-import System.Exit
-import System.IO (stderr, hPutStrLn)
-import System.Console.GetOpt
-import Control.Monad (liftM, foldM)
-
-data Opt
-    = Help
-    | ConfigFile FilePath
-    | Port Int
-    | Debug
-    | Version
-    deriving (Eq)
-
-flags :: [OptDescr Opt]
-flags =
-   [ Option ['h'] ["help"] (NoArg Help)
-        "Print this help message"
-   , Option ['v'] ["version"] (NoArg Version)
-        "Print version information"
-   , Option ['p'] ["port"] (ReqArg (Port . read) "PORT")
-        "Specify port"
-   , Option ['d'] ["debug"] (NoArg Debug)
-        "Print debugging information on each request"
-   , Option ['f'] ["config-file"] (ReqArg ConfigFile "FILE")
-        "Specify configuration file"
-   ]
-
-parseArgs :: [String] -> IO [Opt]
-parseArgs argv = do
-  progname <- getProgName
-  case getOpt Permute flags argv of
-    (opts,_,[])  -> return opts
-    (_,_,errs)   -> hPutStrLn stderr (concat errs ++ usageInfo (usageHeader progname) flags) >>
-                       exitWith (ExitFailure 1)
-
-usageHeader :: String -> String
-usageHeader progname = "Usage:  " ++ progname ++ " [opts...]"
-
-copyrightMessage :: String
-copyrightMessage = "\nCopyright (C) 2008 John MacFarlane\n" ++
-                   "This is free software; see the source for copying conditions.  There is no\n" ++
-                   "warranty, not even for merchantability or fitness for a particular purpose."
-
-handleFlag :: Config -> Opt -> IO Config
-handleFlag conf opt = do
-  progname <- getProgName
-  case opt of
-    Help         -> hPutStrLn stderr (usageInfo (usageHeader progname) flags) >> exitWith ExitSuccess
-    Version      -> hPutStrLn stderr (progname ++ " version " ++ _VERSION ++ copyrightMessage) >> exitWith ExitSuccess
-    Debug        -> return $ conf { debugMode = True }
-    Port p       -> return $ conf { portNumber = p }
-    ConfigFile f -> liftM read (readFile f)
-
-getConfigFromOpts :: IO Config
-getConfigFromOpts = getArgs >>= parseArgs >>= foldM handleFlag defaultConfig
-
diff --git a/Gitit/Convert.hs b/Gitit/Convert.hs
deleted file mode 100644
--- a/Gitit/Convert.hs
+++ /dev/null
@@ -1,71 +0,0 @@
-{-
-Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
-
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
--}
-
-{- Functions for converting markup formats.
--}
-
-module Gitit.Convert ( textToPandoc
-                     , pandocToHtml
-                     )
-where
-import Text.Pandoc
-import Gitit.State
-import Control.Monad.Trans (MonadIO)
-import Text.XHtml
-import Text.Pandoc.Shared (HTMLMathMethod(..))
-
-{-
-removeRawHtmlBlock :: Block -> Block
-removeRawHtmlBlock (RawHtml _) = RawHtml "<!-- raw HTML removed -->"
-removeRawHtmlBlock x = x
--}
-
-readerFor :: PageType -> (String -> Pandoc)
-readerFor pt = case pt of
-                 RST      -> readRST (defaultParserState { stateSanitizeHTML = True, stateSmart = True })
-                 Markdown -> readMarkdown (defaultParserState { stateSanitizeHTML = True, stateSmart = True })
-
-textToPandoc :: PageType -> String -> Pandoc
-textToPandoc pt s = {- processPandoc removeRawHtmlBlock $ -} readerFor pt $ filter (/= '\r') s
-
--- | Convert links with no URL to wikilinks.
-convertWikiLinks :: Inline -> Inline
-convertWikiLinks (Link ref ("", "")) =
-  Link ref (refToUrl ref, "Go to wiki page")
-convertWikiLinks x = x
-
-refToUrl :: [Inline] -> String
-refToUrl = concatMap go
-  where go (Str x)                  = x
-        go (Space)                  = "%20"
-        go (Quoted DoubleQuote xs)  = '"' : (refToUrl xs ++ "\"")
-        go (Quoted SingleQuote xs)  = '\'' : (refToUrl xs ++ "'")
-        go (Apostrophe)             = "'"
-        go (Ellipses)               = "..."
-        go (Math InlineMath t)      = '$' : (t ++ "$")
-        go _                        = ""
-
--- | Converts pandoc document to HTML.
-pandocToHtml :: MonadIO m => Pandoc -> m Html
-pandocToHtml pandocContents = do
-  cfg <- getConfig
-  return $ writeHtml (defaultWriterOptions { writerStandalone = False
-                                           , writerHTMLMathMethod = JsMath (Just "/js/jsMath/easy/load.js")
-                                           , writerTableOfContents = tableOfContents cfg
-                                           }) $ processPandoc convertWikiLinks pandocContents
-
diff --git a/Gitit/Export.hs b/Gitit/Export.hs
deleted file mode 100644
--- a/Gitit/Export.hs
+++ /dev/null
@@ -1,99 +0,0 @@
-{-
-Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
-
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
--}
-
-{- Functions for exporting wiki pages in various formats.
--}
-
-module Gitit.Export ( exportFormats )
-where
-import Text.Pandoc
-import Text.Pandoc.ODT (saveOpenDocumentAsODT)
-import Gitit.Server
-import Gitit.Util (withTempDir)
-import Gitit.State
-import Control.Monad.Trans (liftIO)
-import Text.XHtml (noHtml)
-import qualified Data.ByteString.Lazy as B
-import System.FilePath ((<.>), (</>))
-import Codec.Binary.UTF8.String (encodeString)
-
-defaultRespOptions :: WriterOptions
-defaultRespOptions = defaultWriterOptions { writerStandalone = True, writerWrapText = True }
-
-respondLaTeX :: String -> Pandoc -> Web Response
-respondLaTeX page = ok . setContentType "application/x-latex" . setFilename (page ++ ".tex") . toResponse . encodeString .
-                    writeLaTeX (defaultRespOptions {writerHeader = defaultLaTeXHeader})
-
-respondConTeXt :: String -> Pandoc -> Web Response
-respondConTeXt page = ok . setContentType "application/x-context" . setFilename (page ++ ".tex") . toResponse . encodeString .
-                      writeConTeXt (defaultRespOptions {writerHeader = defaultConTeXtHeader})
-
-respondRTF :: String -> Pandoc -> Web Response
-respondRTF page = ok . setContentType "application/rtf" . setFilename (page ++ ".rtf") . toResponse . encodeString .
-                  writeRTF (defaultRespOptions {writerHeader = defaultRTFHeader})
-
-respondRST :: String -> Pandoc -> Web Response
-respondRST _ = ok . setContentType "text/plain; charset=utf-8" . toResponse . encodeString .
-               writeRST (defaultRespOptions {writerHeader = "", writerReferenceLinks = True})
-
-respondMan :: String -> Pandoc -> Web Response
-respondMan _ = ok . setContentType "text/plain; charset=utf-8" . toResponse . encodeString .
-               writeMan (defaultRespOptions {writerHeader = ""})
-
-respondS5 :: String -> Pandoc -> Web Response
-respondS5 _ = ok . toResponse . writeS5 (defaultRespOptions {writerHeader = defaultS5Header,
-                                                             writerS5 = True, writerIncremental = True})
-
-respondTexinfo :: String -> Pandoc -> Web Response
-respondTexinfo page = ok . setContentType "application/x-texinfo" . setFilename (page ++ ".texi") . toResponse . encodeString .
-                      writeTexinfo (defaultRespOptions {writerHeader = ""})
-
-respondDocbook :: String -> Pandoc -> Web Response
-respondDocbook page = ok . setContentType "application/docbook+xml" . setFilename (page ++ ".xml") . toResponse . encodeString .
-                      writeDocbook (defaultRespOptions {writerHeader = defaultDocbookHeader})
-
-respondMediaWiki :: String -> Pandoc -> Web Response
-respondMediaWiki _ = ok . setContentType "text/plain; charset=utf-8" . toResponse . encodeString .
-                     writeMediaWiki (defaultRespOptions {writerHeader = ""})
-
-respondODT :: String -> Pandoc -> Web Response
-respondODT page doc = do
-  let openDoc = writeOpenDocument (defaultRespOptions {writerHeader = defaultOpenDocumentHeader}) doc
-  contents <- liftIO $ withTempDir "gitit-temp-odt" $ \tempdir -> do
-                let tempfile = tempdir </> page <.> "odt"
-                conf <- getConfig
-                let repoPath = case repository conf of
-                                Git path'   -> path'
-                                Darcs path' -> path'
-                saveOpenDocumentAsODT tempfile repoPath openDoc
-                B.readFile tempfile
-  ok $ setContentType "application/vnd.oasis.opendocument.text" $ setFilename (page ++ ".odt") $ (toResponse noHtml) {rsBody = contents}
-
-exportFormats :: [(String, String -> Pandoc -> Web Response)]   -- (description, writer)
-exportFormats = [ ("LaTeX",     respondLaTeX)
-                , ("ConTeXt",   respondConTeXt)
-                , ("Texinfo",   respondTexinfo)
-                , ("reST",      respondRST)
-                , ("MediaWiki", respondMediaWiki)
-                , ("man",       respondMan)
-                , ("DocBook",   respondDocbook)
-                , ("S5",        respondS5)
-                , ("ODT",       respondODT)
-                , ("RTF",       respondRTF) ]
-
-
diff --git a/Gitit/Framework.hs b/Gitit/Framework.hs
deleted file mode 100644
--- a/Gitit/Framework.hs
+++ /dev/null
@@ -1,303 +0,0 @@
-{-
-Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
--}
-
-{- General framework for defining wiki actions. 
--}
-
-module Gitit.Framework (
-                         Handler
-                       , Recaptcha(..)
-                       , Params(..)
-                       , Command(..)
-                       , getLoggedInUser
-                       , sessionTime
-                       , unlessNoEdit
-                       , unlessNoDelete
-                       , handle
-                       , handlePage
-                       , handleText
-                       , handlePath
-                       , withCommand
-                       , uriPath
-                       , isPage
-                       , isDiscussPage
-                       , isSourceCode
-                       , urlForPage
-                       , pathForPage
-                       , withCommands
-                       , getMimeTypeForExtension
-                       , ifLoggedIn
-                       , validate
-                       )
-where
-import Gitit.Server
-import Gitit.State
-import Text.Pandoc.Shared (substitute)
-import Control.Monad.Reader (mplus)
-import Data.Char (toLower)
-import Data.DateTime
-import Control.Monad.Trans (MonadIO)
-import qualified Data.ByteString.Lazy as B
-import qualified Data.Map as M
-import Data.ByteString.UTF8 (fromString, toString)
-import Data.Maybe (fromMaybe, fromJust)
-import Data.List (intersect, intercalate, isSuffixOf)
-import System.FilePath ((<.>), takeExtension)
-import Codec.Binary.UTF8.String (decodeString, encodeString)
-import Text.Highlighting.Kate
-import Network.HTTP (urlEncode)
-
-type Handler = ServerPart Response
-
-data Recaptcha = Recaptcha {
-    recaptchaChallengeField :: String
-  , recaptchaResponseField  :: String
-  } deriving (Read, Show)
-
-data Params = Params { pUsername     :: String
-                     , pPassword     :: String
-                     , pPassword2    :: String
-                     , pRevision     :: Maybe String
-                     , pDestination  :: String
-                     , pReferer      :: Maybe String
-                     , pUri          :: String
-                     , pForUser      :: String
-                     , pSince        :: Maybe DateTime
-                     , pRaw          :: String
-                     , pLimit        :: Int
-                     , pPatterns     :: [String]
-                     , pGotoPage     :: String
-                     , pEditedText   :: Maybe String
-                     , pMessages     :: [String]
-                     , pFrom         :: Maybe String
-                     , pTo           :: Maybe String
-                     , pFormat       :: String
-                     , pSHA1         :: String
-                     , pLogMsg       :: String
-                     , pEmail        :: String
-                     , pFullName     :: String
-                     , pAccessCode   :: String
-                     , pWikiname     :: String
-                     , pPrintable    :: Bool
-                     , pOverwrite    :: Bool
-                     , pFilename     :: String
-                     , pFileContents :: B.ByteString
-                     , pUser         :: String
-                     , pConfirm      :: Bool 
-                     , pSessionKey   :: Maybe SessionKey
-                     , pRecaptcha    :: Recaptcha
-                     , pPeer         :: String
-                     }  deriving Show
-
-instance FromData Params where
-     fromData = do
-         un <- look "username"       `mplus` return ""
-         pw <- look "password"       `mplus` return ""
-         p2 <- look "password2"      `mplus` return ""
-         rv <- (look "revision" >>= \s ->
-                 return (if null s then Nothing else Just s)) `mplus` return Nothing
-         fu <- look "forUser"        `mplus` return ""
-         si <- (look "since" >>= return . parseDateTime "%Y-%m-%d") `mplus` return Nothing  -- YYYY-mm-dd format
-         ds <- (lookCookieValue "destination") `mplus` return "/"
-         ra <- look "raw"            `mplus` return ""
-         lt <- look "limit"          `mplus` return "100"
-         pa <- look "patterns"       `mplus` return ""
-         gt <- look "gotopage"       `mplus` return ""
-         me <- lookRead "messages"   `mplus` return [] 
-         fm <- (look "from" >>= return . Just) `mplus` return Nothing
-         to <- (look "to" >>= return . Just)   `mplus` return Nothing
-         et <- (look "editedText" >>= return . Just . filter (/= '\r')) `mplus` return Nothing
-         fo <- look "format"         `mplus` return ""
-         sh <- look "sha1"           `mplus` return ""
-         lm <- look "logMsg"         `mplus` return ""
-         em <- look "email"          `mplus` return ""
-         na <- look "fullname"       `mplus` return ""
-         wn <- look "wikiname"       `mplus` return ""
-         pr <- (look "printable" >> return True) `mplus` return False
-         ow <- (look "overwrite" >>= return . (== "yes")) `mplus` return False
-         fn <- (lookInput "file" >>= return . fromMaybe "" . inputFilename) `mplus` return ""
-         fc <- (lookInput "file" >>= return . inputValue) `mplus` return B.empty
-         ac <- look "accessCode"     `mplus` return ""
-         cn <- (look "confirm" >> return True) `mplus` return False
-         sk <- (readCookieValue "sid" >>= return . Just) `mplus` return Nothing
-         rc <- look "recaptcha_challenge_field" `mplus` return ""
-         rr <- look "recaptcha_response_field" `mplus` return ""
-         return $ Params { pUsername     = un
-                         , pPassword     = pw
-                         , pPassword2    = p2
-                         , pRevision     = rv
-                         , pForUser      = fu
-                         , pSince        = si
-                         , pDestination  = ds
-                         , pReferer      = Nothing  -- this gets set by handle...
-                         , pUri          = ""       -- this gets set by handle...
-                         , pRaw          = ra
-                         , pLimit        = read lt
-                         , pPatterns     = words pa
-                         , pGotoPage     = gt
-                         , pMessages     = me
-                         , pFrom         = fm
-                         , pTo           = to
-                         , pEditedText   = et
-                         , pFormat       = fo 
-                         , pSHA1         = sh
-                         , pLogMsg       = lm
-                         , pEmail        = em
-                         , pFullName     = na 
-                         , pWikiname     = wn
-                         , pPrintable    = pr 
-                         , pOverwrite    = ow
-                         , pFilename     = fn
-                         , pFileContents = fc
-                         , pAccessCode   = ac
-                         , pUser         = ""  -- this gets set by ifLoggedIn...
-                         , pConfirm      = cn
-                         , pSessionKey   = sk
-                         , pRecaptcha    = Recaptcha { recaptchaChallengeField = rc, recaptchaResponseField = rr }
-                         , pPeer         = ""  -- this gets set by handle...
-                         }
-
-data Command = Command (Maybe String)
-
-instance FromData Command where
-     fromData = do
-       pairs <- lookPairs
-       return $ case map fst pairs `intersect` commandList of
-                 []          -> Command Nothing
-                 (c:_)       -> Command $ Just c
-               where commandList = ["page", "request", "params", "edit", "showraw", "history",
-                                    "export", "diff", "cancel", "update", "delete", "discuss"]
-
-getLoggedInUser :: MonadIO m => Params -> m (Maybe String)
-getLoggedInUser params = do
-  mbSd <- maybe (return Nothing) getSession $ pSessionKey params
-  let user = case mbSd of
-       Nothing    -> Nothing
-       Just sd    -> Just $ sessionUser sd
-  return $! user
-
-sessionTime :: Int
-sessionTime = 60 * 60     -- session will expire 1 hour after page request
-
-unlessNoEdit :: (String -> Params -> Web Response)
-             -> (String -> Params -> Web Response)
-             -> (String -> Params -> Web Response)
-unlessNoEdit responder fallback =
-  \page params -> do cfg <- getConfig
-                     if page `elem` noEdit cfg
-                        then fallback page params{pMessages = ("Page is locked." : pMessages params)}
-                        else responder page params
-
-unlessNoDelete :: (String -> Params -> Web Response)
-               -> (String -> Params -> Web Response)
-               -> (String -> Params -> Web Response)
-unlessNoDelete responder fallback =
-  \page params ->  do cfg <- getConfig
-                      if page `elem` noDelete cfg
-                         then fallback page params{pMessages = ("Page cannot be deleted." : pMessages params)}
-                         else responder page params
-
-handle :: (String -> Bool) -> Method -> (String -> Params -> Web Response) -> Handler
-handle pathtest meth responder = uriRest $ \uri ->
-  let path' = decodeString $ uriPath uri
-  in  if pathtest path'
-         then withData $ \params ->
-                  [ withRequest $ \req ->
-                      if rqMethod req == meth
-                         then do
-                           let referer = case M.lookup (fromString "referer") (rqHeaders req) of
-                                              Just r | not (null (hValue r)) -> Just $ toString $ head $ hValue r
-                                              _       -> Nothing
-                           let peer = fst $ rqPeer req
-                           responder path' (params { pReferer = referer,
-                                                     pUri = uri,
-                                                     pPeer = peer })
-                         else noHandle ]
-         else anyRequest noHandle
-
-handlePage :: Method -> (String -> Params -> Web Response) -> Handler
-handlePage = handle isPage
-
-handleText :: Method -> (String -> Params -> Web Response) -> Handler
-handleText = handle (\x -> isPage x || isSourceCode x)
-
-handlePath :: String -> Method -> (String -> Params -> Web Response) -> Handler
-handlePath path' = handle (== path')
-
-withCommand :: String -> [Handler] -> Handler
-withCommand command handlers =
-  withData $ \com -> case com of
-                          Command (Just c) | c == command -> handlers
-                          _                               -> []
-
--- | Returns path portion of URI, without initial /.
--- Consecutive spaces are collapsed.  We don't want to distinguish 'Hi There' and 'Hi  There'.
-uriPath :: String -> String
-uriPath = unwords . words . drop 1 . takeWhile (/='?')
-
-isPage :: String -> Bool
-isPage ('_':_) = False
-isPage s = '.' `notElem` s
-
-isDiscussPage :: String -> Bool
-isDiscussPage s = isPage s && ":discuss" `isSuffixOf` s
-
-isSourceCode :: String -> Bool
-isSourceCode = not . null . languagesByExtension . takeExtension
-
-urlForPage :: String -> String
-urlForPage page = '/' : (substitute "%2f" "/" $ urlEncode $ encodeString page)
--- this is needed so that browsers recognize relative URLs correctly
-
-pathForPage :: String -> FilePath
-pathForPage page = page <.> "page"
-
-withCommands :: Method -> [String] -> (String -> Request -> Web Response) -> Handler
-withCommands meth commands page = withRequest $ \req -> do
-  if rqMethod req /= meth
-     then noHandle
-     else if all (`elem` (map fst $ rqInputs req)) commands
-             then page (intercalate "/" $ rqPaths req) req
-             else noHandle
-
-getMimeTypeForExtension :: MonadIO m => String -> m String
-getMimeTypeForExtension ext = do
-  mimes <- queryAppState mimeMap
-  return $ case M.lookup (dropWhile (=='.') $ map toLower ext) mimes of
-                Nothing -> "application/octet-stream"
-                Just t  -> t
-
-ifLoggedIn :: (String -> Params -> Web Response)
-           -> (String -> Params -> Web Response)
-           -> (String -> Params -> Web Response)
-ifLoggedIn responder fallback =
-  \page params -> do user <- getLoggedInUser params
-                     case user of
-                          Nothing  -> do
-                             fallback page (params { pReferer = Just $ pUri params })
-                          Just u   -> do
-                             usrs <- queryAppState users
-                             let e = case M.lookup u usrs of
-                                           Just usr    -> uEmail usr
-                                           Nothing     -> error $ "User '" ++ u ++ "' not found."
-                             -- give the user another hour...
-                             addCookie sessionTime (mkCookie "sid" (show $ fromJust $ pSessionKey params))
-                             responder page (params { pUser = u, pEmail = e })
-
-validate :: [(Bool, String)]   -- ^ list of conditions and error messages
-         -> [String]           -- ^ list of error messages
-validate = foldl go []
-   where go errs (condition, msg) = if condition then msg:errs else errs
-
diff --git a/Gitit/Initialize.hs b/Gitit/Initialize.hs
deleted file mode 100644
--- a/Gitit/Initialize.hs
+++ /dev/null
@@ -1,80 +0,0 @@
-{-
-Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
--}
-
-{- Functions for initializing a Gitit wiki.
--}
-
-module Gitit.Initialize ( createStaticIfMissing, createRepoIfMissing )
-where
-import System.FilePath ((</>), (<.>), takeExtension)
-import Data.FileStore
-import Gitit.State
-import Paths_gitit (getDataFileName)
-import qualified Data.ByteString.Lazy as B
-import Control.Exception (throwIO, try)
-import System.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, getDirectoryContents)
-import System.IO (stderr, hPutStrLn)
-import Control.Monad (unless, forM_, liftM)
-
--- | Create page repository unless it exists.
-createRepoIfMissing :: Config -> IO ()
-createRepoIfMissing conf = do
-  fs <- getFileStore
-  repoExists <- try (initialize fs) >>= \res ->
-    case res of
-         Right _               -> return False
-         Left RepositoryExists -> return True
-         Left e                -> throwIO e >> return False
-  unless repoExists $ do
-    welcomepath <- getDataFileName $ "data" </> "FrontPage.page"
-    welcomecontents <- B.readFile welcomepath
-    helppath <- getDataFileName $ "data" </> "Help.page"
-    helpcontents <- B.readFile helppath
-    -- add front page and help page
-    create fs (frontPage conf <.> "page") (Author "Gitit" "") "Default front page" welcomecontents
-    create fs "Help.page" (Author "Gitit" "") "Default front page" helpcontents
-    hPutStrLn stderr "Created repository"
-
-
--- | Create static directory unless it exists.
-createStaticIfMissing :: FilePath -> IO ()
-createStaticIfMissing staticdir = do
-  staticExists <- doesDirectoryExist staticdir
-  unless staticExists $ do
-
-    let cssdir = staticdir </> "css"
-    createDirectoryIfMissing True cssdir
-    cssDataDir <- getDataFileName "css"
-    cssFiles <- liftM (filter (\f -> takeExtension f == ".css")) $ getDirectoryContents cssDataDir
-    forM_ cssFiles $ \f -> copyFile (cssDataDir </> f) (cssdir </> f)
-
-    let icondir = staticdir </> "img" </> "icons" 
-    createDirectoryIfMissing True icondir
-    iconDataDir <- getDataFileName ("img" </> "icons")
-    iconFiles <- liftM (filter (\f -> takeExtension f == ".png")) $ getDirectoryContents iconDataDir
-    forM_ iconFiles $ \f -> copyFile (iconDataDir </> f) (icondir </> f)
-
-    logopath <- getDataFileName $ "img" </> "gitit-dog.png"
-    copyFile logopath $ staticdir </> "img" </> "logo.png"
-
-    let jsdir = staticdir </> "js"
-    createDirectoryIfMissing True jsdir
-    let javascripts = ["jquery.min.js", "jquery-ui.packed.js",
-                       "folding.js", "dragdiff.js", "preview.js", "search.js", "uploadForm.js"]
-    jsDataDir <- getDataFileName "js"
-    forM_ javascripts $ \f -> copyFile (jsDataDir </> f) (jsdir </> f)
-
-    hPutStrLn stderr $ "Created " ++ staticdir ++ " directory"
-
diff --git a/Gitit/Layout.hs b/Gitit/Layout.hs
deleted file mode 100644
--- a/Gitit/Layout.hs
+++ /dev/null
@@ -1,148 +0,0 @@
-{-
-Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
-
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
--}
-
-{- Functions and data structures for wiki page layout.
--}
-
-module Gitit.Layout ( PageLayout(..)
-                    , Tab(..)
-                    , defaultPageLayout
-                    , formattedPage 
-                    )
-where
-import Data.FileStore
-import Gitit.Server
-import Gitit.Framework
-import Gitit.State
-import Gitit.Util (orIfNull)
-import Gitit.Export (exportFormats)
-import Network.HTTP (urlEncodeVars)
-import Codec.Binary.UTF8.String (encodeString)
-import qualified Text.StringTemplate as T
-import Text.XHtml hiding ( (</>), dir, method, password, rev )
-import Data.Maybe (isNothing, isJust, mapMaybe, fromJust)
-import Data.List (isSuffixOf)
-import Prelude hiding (catch)
-import Control.Exception (throwIO, catch)
-import Control.Monad.Trans (liftIO)
-
--- | Abstract representation of page layout (tabs, scripts, etc.)
-data PageLayout = PageLayout
-  { pgTitle          :: String
-  , pgScripts        :: [String]
-  , pgShowPageTools  :: Bool
-  , pgTabs           :: [Tab]
-  , pgSelectedTab    :: Tab
-  }
-
-data Tab = ViewTab | EditTab | HistoryTab | DiscussTab | DiffTab deriving (Eq, Show)
-
-defaultPageLayout :: PageLayout
-defaultPageLayout = PageLayout
-  { pgTitle          = ""
-  , pgScripts        = []
-  , pgShowPageTools  = True
-  , pgTabs           = [ViewTab, EditTab, HistoryTab, DiscussTab]
-  , pgSelectedTab    = ViewTab
-  }
-
--- | Returns formatted page
-formattedPage :: PageLayout -> String -> Params -> Html -> Web Response
-formattedPage layout page params htmlContents = do
-  let rev = pRevision params
-  let path' = if isPage page then pathForPage page else page
-  fs <- getFileStore
-  sha1 <- case rev of
-             Nothing -> liftIO $ catch (latest fs path')
-                                       (\e -> if e == NotFound
-                                                 then return ""
-                                                 else throwIO e)
-             Just r  -> return r
-  user <- getLoggedInUser params
-  let javascriptlinks = if null (pgScripts layout)
-                           then ""
-                           else renderHtmlFragment $ concatHtml $ map
-                                  (\x -> script ! [src ("/js/" ++ x), thetype "text/javascript"] << noHtml)
-                                  (["jquery.min.js", "jquery-ui.packed.js"] ++ pgScripts layout)
-  let pageTitle = pgTitle layout `orIfNull` page
-  let tabli tab = if tab == pgSelectedTab layout
-                     then li ! [theclass "selected"]
-                     else li
-  let origPage s = if ":discuss" `isSuffixOf` s then take (length s - 8) s else s
-  let linkForTab HistoryTab = Just $ tabli HistoryTab << anchor ! [href $ urlForPage page ++ "?history" ++ 
-                                                                    case rev of { Just r -> "&revision" ++ r; Nothing -> "" }] << "history"
-      linkForTab DiffTab    = Just $ tabli DiffTab << anchor ! [href ""] << "diff"
-      linkForTab ViewTab    = if isDiscussPage page
-                                 then Just $ tabli DiscussTab << anchor ! [href $ urlForPage $ origPage page] << "page"
-                                 else Just $ tabli ViewTab << anchor ! [href $ urlForPage page ++
-                                                                    case rev of { Just r -> "?revision=" ++ r; Nothing -> "" }] << "view"
-      linkForTab DiscussTab = if isDiscussPage page
-                                 then Just $ tabli ViewTab << anchor ! [href $ urlForPage page] << "discuss"
-                                 else if isPage page
-                                      then Just $ tabli DiscussTab << anchor ! [href $ urlForPage page ++ "?discuss"] << "discuss"
-                                      else Nothing
-      linkForTab EditTab    = if isPage page
-                                 then Just $ tabli EditTab << anchor ! [href $ urlForPage page ++ "?edit" ++
-                                              (case rev of
-                                                    Just r   -> "&revision=" ++ r ++ "&" ++ urlEncodeVars [("logMsg", "Revert to " ++ r)]
-                                                    Nothing  -> "")] <<
-                                             if isNothing rev then "edit" else "revert"
-                                 else Nothing
-  let tabs = ulist ! [theclass "tabs"] << mapMaybe linkForTab (pgTabs layout)
-  let searchbox = gui ("/_search") ! [identifier "searchform"] <<
-                         [ textfield "patterns"
-                         , submit "search" "Search" ]
-  let gobox     = gui ("/_go") ! [identifier "goform"] <<
-                         [ textfield "gotopage"
-                         , submit "go" "Go" ]
-  let messages = pMessages params
-  let htmlMessages = if null messages
-                        then noHtml
-                        else ulist ! [theclass "messages"] << map (li <<) messages
-  templ <- queryAppState template
-  let filledTemp = T.render $
-                   T.setAttribute "pagetitle" pageTitle $
-                   T.setAttribute "javascripts" javascriptlinks $
-                   T.setAttribute "pagename" page $
-                   (case user of
-                         Just u     -> T.setAttribute "user" u
-                         Nothing    -> id) $
-                   (if isPage page then T.setAttribute "ispage" "true" else id) $
-                   (if pgShowPageTools layout then T.setAttribute "pagetools" "true" else id) $
-                   (if pPrintable params then T.setAttribute "printable" "true" else id) $
-                   (if isJust rev then T.setAttribute "nothead" "true" else id) $
-                   (if isJust rev then T.setAttribute "revision" (fromJust rev) else id) $
-                   T.setAttribute "sha1" sha1 $
-                   T.setAttribute "searchbox" (renderHtmlFragment (searchbox +++ gobox)) $
-                   T.setAttribute "exportbox" (renderHtmlFragment $  exportBox page params) $
-                   T.setAttribute "tabs" (renderHtmlFragment tabs) $
-                   T.setAttribute "messages" (renderHtmlFragment htmlMessages) $
-                   T.setAttribute "content" (renderHtmlFragment htmlContents) $
-                   templ
-  ok $ setContentType "text/html" $ toResponse $ encodeString filledTemp
-
-exportBox :: String -> Params -> Html
-exportBox page params | isPage page =
-  let rev = pRevision params
-  in  gui (urlForPage page) ! [identifier "exportbox"] << 
-        ([ textfield "revision" ! [thestyle "display: none;", value (fromJust rev)] | isJust rev ] ++
-         [ select ! [name "format"] <<
-             map ((\f -> option ! [value f] << f) . fst) exportFormats
-         , submit "export" "Export" ])
-exportBox _ _ = noHtml
-
diff --git a/Gitit/Server.hs b/Gitit/Server.hs
deleted file mode 100644
--- a/Gitit/Server.hs
+++ /dev/null
@@ -1,218 +0,0 @@
-{-# OPTIONS_GHC -fno-warn-orphans #-}
-{-
-Copyright (C) 2008 John MacFarlane <jgm@berkeley.edu>
-
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
--}
-
-{- Re-exports HAppS functions needed by gitit, including 
-   replacements for HAppS functions that don't handle UTF-8 properly,
-   new functions for setting headers and zipping contents and for looking up IP
-   addresses, and a fix for broken HAppS cookie parsing.
--}
-
-module Gitit.Server
-          ( look
-          , lookPairs
-          , lookRead
-          , mkCookie
-          , filterIf
-          , gzipBinary
-          , acceptsZip
-          , withExpiresHeaders
-          , setContentType
-          , setFilename
-          , lookupIPAddr
-          , readMimeTypesFile
-          , cookieFixer
-          -- re-exported HAppS functions
-          , ok
-          , toResponse
-          , Response(..)
-          , Method(..)
-          , Request(..)
-          , Input(..)
-          , HeaderPair(..)
-          , Web
-          , ServerPart
-          , FromData(..)
-          , waitForTermination
-          , Conf(..)
-          , simpleHTTP
-          , fileServe
-          , dir
-          , multi
-          , seeOther
-          , withData
-          , withRequest
-          , anyRequest
-          , noHandle
-          , uriRest
-          , lookInput
-          , addCookie
-          , lookCookieValue
-          , readCookieValue
-          )
-where
-import HAppS.Server hiding (look, lookRead, lookPairs, mkCookie, getCookies)
-import qualified HAppS.Server (mkCookie)
-import HAppS.Server.Cookie (Cookie(..))
-import Network.Socket (getAddrInfo, defaultHints, addrAddress)
-import System.IO (stderr, hPutStrLn)
-import Text.Pandoc.CharacterReferences (decodeCharacterReferences)
-import Control.Monad (liftM)
-import Control.Monad.Reader
-import Data.DateTime
-import Data.ByteString.Lazy.UTF8 (toString)
-import Codec.Binary.UTF8.String (encodeString)
-import qualified Data.ByteString.Char8 as C
-import Data.Char (chr, toLower)
-import Data.List ((\\))
-import Data.Maybe
-import qualified Data.Map as M
-import Codec.Compression.GZip (compress)
-import Control.Applicative
-import Control.Monad (MonadPlus(..), ap)
--- Hide Parsec's definitions of some Applicative functions.
-import Text.ParserCombinators.Parsec hiding (many, optional, (<|>), token)
-
--- Contents of an HTML text area or text field generated by Text.XHtml
--- will often contain decimal character references.  We want to convert these
--- to regular unicode characters.  We also need to use toString to
--- convert from UTF-8, since HAppS doesn't do this.
-
-look :: String -> RqData String
-look = liftM (decodeCharacterReferences . toString) . lookBS
-
-lookPairs :: RqData [(String,String)]
-lookPairs = asks fst >>= return . map (\(n,vbs)->(n,toString $ inputValue vbs))
-
-lookRead :: Read a => String -> RqData a
-lookRead = liftM read . look
-
-mkCookie :: String -> String -> Cookie
-mkCookie name = HAppS.Server.mkCookie name . encodeString
-
--- Functions for zipping responses and setting headers.
-
-filterIf :: (Request -> Bool) -> (Response -> Response) -> ServerPart Response -> ServerPart Response
-filterIf test filt sp =
-  let handler = unServerPartT sp
-  in  withRequest $ \req ->
-      if test req
-         then liftM filt $ handler req
-         else handler req
-
-gzipBinary :: Response -> Response
-gzipBinary r@(Response {rsBody = b}) =  setHeader "Content-Encoding" "gzip" $ r {rsBody = compress b}
-
-acceptsZip :: Request -> Bool
-acceptsZip req = isJust $ M.lookup (C.pack "accept-encoding") (rqHeaders req)
-
-getCacheTime :: IO (Maybe DateTime)
-getCacheTime = liftM (Just . addMinutes 360) $ getCurrentTime
-
-withExpiresHeaders :: ServerPart Response -> ServerPart Response
-withExpiresHeaders sp = require getCacheTime $ \t -> [liftM (setHeader "Expires" $ formatDateTime "%a, %d %b %Y %T GMT" t) sp]
-
-setContentType :: String -> Response -> Response
-setContentType = setHeader "Content-Type"
-
-setFilename :: String -> Response -> Response
-setFilename = setHeader "Content-Disposition" . \fname -> "attachment: filename=\"" ++ fname ++ "\""
-
--- IP lookup
-
-lookupIPAddr :: String -> IO (Maybe String)
-lookupIPAddr hostname = do
-  addrs <- getAddrInfo (Just defaultHints) (Just hostname) Nothing
-  if null addrs
-     then return Nothing
-     else return $ Just $ takeWhile (/=':') $ show $ addrAddress $ head addrs
-
-
--- mime types
-
--- | Read a file associating mime types with extensions, and return a
--- map from extensions to types. Each line of the file consists of a
--- mime type, followed by space, followed by a list of zero or more
--- extensions, separated by spaces. Example: text/plain txt text
-readMimeTypesFile :: FilePath -> IO (M.Map String String)
-readMimeTypesFile f = catch (readFile f >>= return . foldr go M.empty . map words . lines) $
-                            handleMimeTypesFileNotFound
-     where go []     m = m  -- skip blank lines
-           go (x:xs) m = foldr (\ext m' -> M.insert ext x m') m xs
-           handleMimeTypesFileNotFound e = do
-             hPutStrLn stderr $ "Could not read mime types file: " ++ f
-             hPutStrLn stderr $ show e
-             hPutStrLn stderr $ "Using defaults instead."
-             return mimeTypes
-
------ the following code is from the HAppSHelpers package, 0.10,
------ (C) 2008 Thomas Hartman.
------ Needed until HAppS Server cookie parsing is fixed.
-
-instance Applicative (GenParser s a) where
-    pure = return
-    (<*>) = ap
-
-instance Alternative (GenParser s a) where
-    empty = mzero
-    (<|>) = mplus
-
-parseCookiesM :: (Monad m) => String -> m [Cookie]
-parseCookiesM str = either (fail "Invalid cookie syntax!") return $ parse cookiesParser str str
-
-cookiesParser :: GenParser Char st [Cookie]
-cookiesParser = av_pairs
-    where -- Parsers based on RFC 2109
-          av_pairs      = (:) <$> av_pair <*> many (char ';' *>  av_pair)
-          av_pair       = cookie <$> attr <*> option "" (char '=' *> value)
-          attr          = spaces *> token
-          value         = word
-          word          = incomp_token <|> quoted_string
-
-          -- Parsers based on RFC 2068
-          token         = many1 $ oneOf ((chars \\ ctl) \\ tspecials)
-          quoted_string = char '"' *> many (oneOf qdtext) <* char '"'
-
-          -- Custom parser, incompatible with RFC 2068, but very  forgiving ;)
-          incomp_token  = many1 $ oneOf ((chars \\ ctl) \\ "\";")
-
-          -- Primitives from RFC 2068
-          tspecials     = "()<>@,;:\\\"/[]?={} \t"
-          ctl           = map chr (127:[0..31])
-          chars         = map chr [0..127]
-          octet         = map chr [0..255]
-          text          = octet \\ ctl
-          qdtext        = text \\ "\""
-
-cookie :: String -> String -> Cookie
-cookie key value = Cookie "" "" "" (low key) value
-
-cookieFixer :: ServerPartT m a -> ServerPartT m a
-cookieFixer (ServerPartT sp) = ServerPartT $ \request -> sp (request { rqCookies = (fixedCookies request) } )
-    where
-      fixedCookies request = [ (cookieName c, c) | cl <- fromMaybe [] (fmap getCookies (getHeader "Cookie" (rqHeaders request))), c <- cl ]
-
--- | Get all cookies from the HTTP request. The cookies are ordered per RFC from
--- the most specific to the least specific. Multiple cookies with the same
--- name are allowed to exist.
-getCookies :: Monad m => C.ByteString -> m [Cookie]
-getCookies header | C.null header = return []
-                  | otherwise     = parseCookiesM (C.unpack header)
-
-low :: String -> String
-low = map toLower
diff --git a/Gitit/State.hs b/Gitit/State.hs
deleted file mode 100644
--- a/Gitit/State.hs
+++ /dev/null
@@ -1,258 +0,0 @@
-{-
-Copyright (C) 2008 John MacFarlane <jgm@berkeley.edu>
-
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
--}
-
-{- Functions for maintaining user list and session state.
--}
-
-module Gitit.State where
-
-import qualified Data.Map as M
-import System.Random (randomRIO)
-import Data.Digest.Pure.SHA (sha512, showDigest)
-import qualified Data.ByteString.Lazy.UTF8 as L (fromString)
-import Data.IORef
-import System.IO.Unsafe (unsafePerformIO)
-import System.Directory (doesFileExist)
-import System.FilePath ((</>))
-import Control.Monad.Trans (MonadIO(), liftIO)
-import Control.Monad (replicateM, liftM)
-import Control.Exception (try, throwIO)
-import Data.FileStore
-import Data.List (intercalate)
-import Text.XHtml (Html)
-import qualified Text.StringTemplate as T
-import Gitit.Server (readMimeTypesFile)
-
-appstate :: IORef AppState
-appstate = unsafePerformIO $  newIORef $ AppState { sessions = undefined
-                                                  , users = undefined
-                                                  , config = undefined
-                                                  , filestore = undefined
-                                                  , mimeMap = undefined
-                                                  , cache = undefined
-                                                  , template = undefined
-                                                  , jsMath = undefined }
-
-initializeAppState :: MonadIO m => Config -> M.Map String User -> T.StringTemplate String -> m ()
-initializeAppState conf users' templ = do
-  mimeMapFromFile <- liftIO $ readMimeTypesFile (mimeTypesFile conf)
-  jsMathExists <- liftIO $ doesFileExist $ staticDir conf </> "js" </> "jsMath" </> "easy" </> "load.js"
-  updateAppState $ \s -> s { sessions  = Sessions M.empty
-                           , users     = users'
-                           , config    = conf
-                           , filestore = case repository conf of
-                                              Git fs   -> gitFileStore fs
-                                              Darcs fs -> darcsFileStore fs
-                           , mimeMap   = mimeMapFromFile
-                           , cache     = M.empty
-                           , template  = templ
-                           , jsMath    = jsMathExists }
-
-updateAppState :: MonadIO m => (AppState -> AppState) -> m () 
-updateAppState fn = liftIO $! atomicModifyIORef appstate $ \st -> (fn st, ())
-
-queryAppState :: MonadIO m => (AppState -> a) -> m a
-queryAppState fn = liftIO $! readIORef appstate >>= return . fn
-
-data Repository = Git FilePath 
-                | Darcs FilePath 
-                deriving (Read, Show)
-
-data PageType = Markdown | RST
-                deriving (Read, Show)
-
--- | Data structure for information read from config file.
-data Config = Config {
-  repository          :: Repository,               -- file store for pages
-  defaultPageType     :: PageType,                 -- the default page markup type for this wiki
-  userFile            :: FilePath,                 -- path of users database 
-  templateFile        :: FilePath,                 -- path of page template file
-  staticDir           :: FilePath,                 -- path of static directory
-  tableOfContents     :: Bool,                     -- should each page have an automatic table of contents?
-  maxUploadSize       :: Integer,                  -- maximum size of pages and file uploads
-  portNumber          :: Int,                      -- port number to serve content on
-  debugMode           :: Bool,                     -- should debug info be printed to the console?
-  frontPage           :: String,                   -- the front page of the wiki
-  noEdit              :: [String],                 -- pages that cannot be edited through the web interface
-  noDelete            :: [String],                 -- pages that cannot be deleted through the web interface
-  accessQuestion      :: Maybe (String, [String]), -- if Nothing, then anyone can register for an account.
-                                                   -- if Just (prompt, answers), then a user will be given the prompt
-                                                   -- and must give one of the answers in order to register.
-  useRecaptcha        :: Bool,                     -- use ReCAPTCHA service to provide captchas for user registration.
-  recaptchaPublicKey  :: String,
-  recaptchaPrivateKey :: String,
-  mimeTypesFile       :: FilePath                  -- path of file associating mime types with file extensions
-  } deriving (Read, Show)
-
-defaultConfig :: Config
-defaultConfig = Config {
-  repository          = Git "wikidata",
-  defaultPageType     = Markdown,
-  userFile            = "gitit-users",
-  templateFile        = "template.html",
-  staticDir           = "static",
-  tableOfContents     = True,
-  maxUploadSize       = 100000,
-  portNumber          = 5001,
-  debugMode           = False,
-  frontPage           = "Front Page",
-  noEdit              = ["Help"],
-  noDelete            = ["Help", "Front Page"],
-  accessQuestion      = Nothing,
-  useRecaptcha        = False,
-  recaptchaPublicKey  = "",
-  recaptchaPrivateKey = "",
-  mimeTypesFile       = "/etc/mime.types"
-  }
-
-data CachedPage = CachedPage {
-    cpContents        :: Html
-  , cpRevisionId      :: RevisionId
-  } deriving Show
-
-type SessionKey = Integer
-
-data SessionData = SessionData {
-  sessionUser :: String
-} deriving (Read,Show,Eq)
-
-data Sessions a = Sessions {unsession::M.Map SessionKey a}
-  deriving (Read,Show,Eq)
-
--- Password salt hashedPassword
-data Password = Password { pSalt :: String, pHashed :: String }
-  deriving (Read,Show,Eq)
-
-data User = User {
-  uUsername :: String,
-  uPassword :: Password,
-  uEmail    :: String
-} deriving (Show,Read)
-
-data AppState = AppState {
-  sessions  :: Sessions SessionData,
-  users     :: M.Map String User,
-  config    :: Config,
-  filestore :: FileStore,
-  mimeMap   :: M.Map String String,
-  cache     :: M.Map String CachedPage,
-  template  :: T.StringTemplate String,
-  jsMath    :: Bool
-}
-
-lookupCache :: MonadIO m => String -> (Maybe RevisionId) -> m (Maybe CachedPage)
-lookupCache file (Just revid) = do
-  c <- queryAppState cache
-  fs <- getFileStore
-  case M.lookup file c of
-       Just cp | idsMatch fs (cpRevisionId cp) revid ->
-                   return $ Just cp
-       _        -> return Nothing
-lookupCache file Nothing = do
-  fs <- getFileStore
-  latestRes <- liftIO $ try (latest fs file)
-  case latestRes of
-       Right latestid -> do
-         c <- queryAppState cache
-         case M.lookup file c of
-              Just cp | idsMatch fs (cpRevisionId cp) latestid ->
-                          return $ Just cp
-              _        -> return Nothing
-       Left NotFound   -> return Nothing
-       Left e          -> liftIO $ throwIO e
-
-cacheContents :: MonadIO m => String -> RevisionId -> Html -> m ()
-cacheContents file revid contents = do
-  c <- queryAppState cache
-  let newpage = CachedPage { cpContents = contents
-                           , cpRevisionId = revid }
-  let newcache = M.insert file newpage c
-  updateAppState $ \s -> s { cache = newcache }
-
-mkUser :: String   -- username
-       -> String   -- email
-       -> String   -- unhashed password
-       -> IO User
-mkUser uname email pass = do
-  salt <- genSalt
-  return $ User { uUsername = uname,
-                  uPassword = Password { pSalt = salt, pHashed = hashPassword salt pass },
-                  uEmail = email }
-
-genSalt :: IO String
-genSalt = replicateM 32 $ randomRIO ('0','z')
-
-hashPassword :: String -> String -> String
-hashPassword salt pass = showDigest $ sha512 $ L.fromString $ salt ++ pass
-
-authUser :: MonadIO m => String -> String -> m Bool
-authUser name pass = do
-  users' <- queryAppState users
-  case M.lookup name users' of
-       Just u  -> do
-         let salt = pSalt $ uPassword u
-         let hashed = pHashed $ uPassword u
-         return $ hashed == hashPassword salt pass
-       Nothing -> return False 
-
-isUser :: MonadIO m => String -> m Bool
-isUser name = liftM (M.member name) $ queryAppState users
-
-addUser :: MonadIO m => String -> User -> m () 
-addUser uname user = updateAppState (\s -> s { users = M.insert uname user (users s) }) >>
-                     liftIO writeUserFile
-
-delUser :: MonadIO m => String -> m ()
-delUser uname = updateAppState (\s -> s { users = M.delete uname (users s) }) >>
-                liftIO writeUserFile
-
-writeUserFile :: IO ()
-writeUserFile = do
-  conf <- getConfig
-  usrs <- queryAppState users
-  liftIO $ writeFile (userFile conf) $ "[" ++ intercalate "\n," (map show $ M.toList usrs) ++ "\n]"
-
-getUser :: MonadIO m => String -> m (Maybe User)
-getUser uname = liftM (M.lookup uname) $ queryAppState users
-
-isSession :: MonadIO m => SessionKey -> m Bool
-isSession key = liftM (M.member key . unsession) $ queryAppState sessions
-
-setSession :: MonadIO m => SessionKey -> SessionData -> m ()
-setSession key u = updateAppState $ \s -> s { sessions = Sessions . M.insert key u . unsession $ sessions s }
-
-newSession :: MonadIO m => SessionData -> m SessionKey
-newSession u = do
-  key <- liftIO $ randomRIO (0, 1000000000)
-  setSession key u
-  return key
-
-delSession :: MonadIO m => SessionKey -> m ()
-delSession key = updateAppState $ \s -> s { sessions = Sessions . M.delete key . unsession $ sessions s }
-
-getSession :: MonadIO m => SessionKey -> m (Maybe SessionData)
-getSession key = queryAppState $ M.lookup key . unsession . sessions
-
-getConfig :: MonadIO m => m Config
-getConfig = queryAppState config
-
-getFileStore :: MonadIO m => m FileStore
-getFileStore = queryAppState filestore
-
-getDefaultPageType :: MonadIO m => m PageType
-getDefaultPageType = liftM defaultPageType (queryAppState config)
diff --git a/Gitit/Util.hs b/Gitit/Util.hs
deleted file mode 100644
--- a/Gitit/Util.hs
+++ /dev/null
@@ -1,57 +0,0 @@
-{-
-Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
-This program is free software; you can redistribute it and/or modify
-it under the terms of the GNU General Public License as published by
-the Free Software Foundation; either version 2 of the License, or
-(at your option) any later version.
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-GNU General Public License for more details.
-You should have received a copy of the GNU General Public License
-along with this program; if not, write to the Free Software
-Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
--}
-
-{- Utility functions for Gitit.
--}
-
-module Gitit.Util ( withTempDir
-                  , orIfNull
-                  , consolidateHeads
-                  )
-where
-import System.Directory (getTemporaryDirectory, createDirectory, removeDirectoryRecursive)
-import Control.Exception (bracket)
-import System.FilePath ((</>), (<.>))
-import System.IO.Error (isAlreadyExistsError)
-import Control.Monad.Trans (liftIO)
-import Data.List (nub)
-
--- | Perform a function in a temporary directory and clean up.
-withTempDir :: FilePath -> (FilePath -> IO a) -> IO a
-withTempDir baseName = bracket (createTempDir 0 baseName) (removeDirectoryRecursive)
-
--- | Create a temporary directory with a unique name.
-createTempDir :: Integer -> FilePath -> IO FilePath
-createTempDir num baseName = do
-  sysTempDir <- getTemporaryDirectory
-  let dirName = sysTempDir </> baseName <.> show num
-  liftIO $ catch (createDirectory dirName >> return dirName) $
-      \e -> if isAlreadyExistsError e
-               then createTempDir (num + 1) baseName
-               else ioError e
-
--- | Returns a string, if it is not null, or a backup, if it is.
-orIfNull :: String -> String -> String
-orIfNull str backup = if null str then backup else str
-
--- | Map a list of nonempty lists onto a list of pairs of list heads and list of tails.
--- e.g. [[1,2],[1],[2,1]] -> [(1,[[2],[]]), (2,[[1]])]
-consolidateHeads :: Eq a => [[a]] -> [(a,[[a]])]
-consolidateHeads lst =
-  let heads = nub $ map head lst
-      tailsFor h = map tail [l | l <- lst, head l == h]
-  in  map (\h -> (h, tailsFor h)) heads
-
-
diff --git a/Network/Gitit.hs b/Network/Gitit.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit.hs
@@ -0,0 +1,212 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- | Functions for embedding a gitit wiki into a Happstack application.
+
+The following is a minimal standalone wiki program:
+
+> import Network.Gitit
+> import Happstack.Server.SimpleHTTP
+> 
+> main = do
+>   conf <- getDefaultConfig
+>   createStaticIfMissing conf
+>   createTemplateIfMissing conf
+>   createRepoIfMissing conf
+>   initializeGititState conf
+>   simpleHTTP nullConf{port = 5001} $ wiki conf
+
+Here is a more complex example, which serves different wikis
+under different paths, and uses a custom authentication scheme:
+
+> import Network.Gitit
+> import Control.Monad
+> import Text.XHtml hiding (dir)
+> import Happstack.Server.SimpleHTTP
+> import My.Auth.System (myGetUser, myLoginUser, myLogoutUser)
+> 
+> type WikiSpec = (String, FileStoreType, PageType)
+> 
+> wikis = [ ("markdownWiki", Git, Markdown)
+>         , ("latexWiki", Darcs, LaTeX) ]
+> 
+> -- custom authentication
+> withUser :: Handler -> Handler
+> withUser handler = do
+>   user <- myGetUser
+>   localRq (setHeader "REMOTE_USER" user) handler
+>
+> myAuthHandler = msum
+>   [ dir "_login" myLoginUser
+>   , dir "_logout" myLogoutUser ]
+>
+> handlerFor :: Config -> WikiSpec -> ServerPart Response
+> handlerFor conf (path', fstype, pagetype) = dir path' $
+>   wiki conf{ repositoryPath = path'
+>            , repositoryType = fstype
+>            , defaultPageType = pagetype}
+>
+> indexPage :: ServerPart Response
+> indexPage = ok $ toResponse $
+>   (p << "Wiki index") +++
+>   ulist << map (\(path', _, _) -> li << hotlink (path' ++ "/") << path') wikis
+> 
+> main = do
+>   conf <- getDefaultConfig
+>   let conf' = conf{authHandler = myAuthHandler}
+>   forM wikis $ \(path', fstype, pagetype) -> do
+>     let conf'' = conf'{ repositoryPath = path'
+>                       , repositoryType = fstype
+>                       , defaultPageType = pagetype
+>                       }
+>     createStaticIfMissing conf''
+>     createRepoIfMissing conf''
+>   createTemplateIfMissing conf'
+>   initializeGititState conf'
+>   simpleHTTP nullConf{port = 5001} $
+>     (nullDir >> indexPage) `mplus` msum (map (handlerFor conf') wikis)
+
+
+-}
+
+module Network.Gitit (
+                     -- * Wiki handlers
+                       wiki
+                     , reloadTemplates
+                     , runHandler
+                     -- * Initialization
+                     , module Network.Gitit.Initialize
+                     -- * Configuration
+                     , module Network.Gitit.Config
+                     , loginUserForm
+                     -- * Types
+                     , module Network.Gitit.Types
+                     -- * Tools for building handlers
+                     , module Network.Gitit.Framework
+                     , module Network.Gitit.ContentTransformer
+                     , getFileStore
+                     , getUser
+                     , getConfig
+                     )
+where
+import Network.Gitit.Types
+import Network.Gitit.Server
+import Network.Gitit.Framework
+import Network.Gitit.Handlers
+import Network.Gitit.Initialize
+import Network.Gitit.Config
+import Network.Gitit.State (getFileStore, getUser, getConfig)
+import Network.Gitit.ContentTransformer
+import Network.Gitit.Authentication (loginUserForm)
+import Paths_gitit (getDataFileName)
+import Control.Monad.Reader
+import Prelude hiding (readFile)
+import qualified Data.ByteString.Char8 as B
+import System.FilePath ((</>))
+
+-- | Happstack handler for a gitit wiki.
+wiki :: Config -> ServerPart Response
+wiki conf = do
+  let static = staticDir conf
+  defaultStatic <- liftIO $ getDataFileName $ "data" </> "static"
+  -- if file not found in staticDir, we check also in the data/static
+  -- directory, which contains defaults
+  let staticHandler = withExpiresHeaders $
+        fileServeStrict' [] static `mplus` fileServeStrict' [] defaultStatic
+  let handlers = [debugHandler | debugMode conf] ++ (authHandler conf : wikiHandlers)
+  let fs = filestoreFromConfig conf
+  let ws = WikiState { wikiConfig = conf, wikiFileStore = fs }
+  if compressResponses conf
+     then compressedResponseFilter
+     else return ""
+  staticHandler `mplus` runHandler ws (withUser conf $ msum handlers)
+
+-- | Like 'fileServeStrict', but if file is not found, fail instead of
+-- returning a 404 error.
+fileServeStrict' :: [FilePath] -> FilePath -> ServerPart Response
+fileServeStrict' ps p = do
+  rq <- askRq
+  resp <- fileServeStrict ps p
+  if rsCode resp == 404 || last (rqUri rq) == '/'
+     then mzero  -- pass through if not found or directory index
+     else do
+       -- turn off compresion filter unless it's text
+       case getHeader "Content-Type" resp of
+            Just ct | B.pack "text/" `B.isPrefixOf` ct -> return resp
+            _ -> ignoreFilters >> return resp
+
+wikiHandlers :: [Handler]
+wikiHandlers =
+  [ -- redirect /wiki -> /wiki/ when gitit is being served at /wiki
+    -- so that relative wikilinks on the page will work properly:
+    guardBareBase >> getWikiBase >>= \b -> movedPermanently (b ++ "/") (toResponse ())
+  , dir "_user"     currentUser
+  , dir "_activity" showActivity
+  , dir "_go"       goToPage
+  , dir "_search"   searchResults
+  , dir "_upload"   $ methodOnly GET  >> requireUser uploadForm 
+  , dir "_upload"   $ methodOnly POST >> requireUser uploadFile
+  , dir "_random"   $ methodOnly GET  >> randomPage
+  , dir "_index"    indexPage
+  , dir "_feed"     feedHandler
+  , dir "_category" categoryPage
+  , dir "_categories" categoryListPage
+  , dir "_expire"     expireCache
+  , dir "_showraw"  $ msum
+      [ showRawPage
+      , guardPath isSourceCode >> showFileAsText ]
+  , dir "_history"  $ msum
+      [ showPageHistory
+      , guardPath isSourceCode >> showFileHistory ]
+  , dir "_edit" $ requireUser (unlessNoEdit editPage showPage)
+  , dir "_diff" $ msum
+      [ showPageDiff
+      , guardPath isSourceCode >> showFileDiff ]
+  , dir "_discuss" discussPage
+  , dir "_delete" $ msum
+      [ methodOnly GET  >>
+          requireUser (unlessNoDelete confirmDelete showPage)
+      , methodOnly POST >>
+          requireUser (unlessNoDelete deletePage showPage) ]
+  , dir "_preview" preview
+  , guardIndex >> indexPage
+  , guardCommand "export" >> exportPage
+  , methodOnly POST >> guardCommand "cancel" >> showPage
+  , methodOnly POST >> guardCommand "update" >>
+      requireUser (unlessNoEdit updatePage showPage)
+  , methodOnly GET >> showPage
+  , guardPath isSourceCode >> methodOnly GET >> showHighlightedSource
+  , handleAny
+  , guardPath isPage >> createPage
+  ]
+
+-- | Recompiles the gitit templates.
+reloadTemplates :: ServerPart Response
+reloadTemplates = do
+  liftIO recompilePageTemplate
+  ok $ toResponse "Page templates have been recompiled."
+
+-- | Converts a gitit Handler into a standard happstack ServerPart.
+runHandler :: WikiState -> Handler -> ServerPart Response
+runHandler = mapServerPartT . unpackReaderT
+
+unpackReaderT:: (Monad m)
+    => c 
+    -> (ReaderT c m) (Maybe ((Either b a), FilterFun b))
+    -> m (Maybe ((Either b a), FilterFun b))
+unpackReaderT st handler = runReaderT handler st
diff --git a/Network/Gitit/Authentication.hs b/Network/Gitit/Authentication.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Authentication.hs
@@ -0,0 +1,423 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>,
+                   Henry Laxen <nadine.and.henry@pobox.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- Handlers for registering and authenticating users.
+-}
+
+module Network.Gitit.Authentication (formAuthHandlers, httpAuthHandlers, loginUserForm) where
+
+import Network.Gitit.State
+import Network.Gitit.Types
+import Network.Gitit.Framework
+import Network.Gitit.Layout
+import Network.Gitit.Server
+import Network.Gitit.Util
+import Network.Captcha.ReCaptcha (captchaFields, validateCaptcha)
+import Text.XHtml hiding ( (</>), dir, method, password, rev )
+import qualified Text.XHtml as X ( password )
+import System.Process (readProcessWithExitCode)
+import Control.Monad (unless, liftM)
+import Control.Monad.Trans (MonadIO(), liftIO)
+import System.Exit
+import System.Log.Logger (logM, Priority(..))
+import Data.Char (isAlphaNum, isAlpha, isAscii)
+import Text.Pandoc.Shared (substitute)
+import Data.Maybe (isJust, fromJust)
+import Network.URL (encString, exportURL, add_param, importURL)
+import Network.BSD (getHostName)
+import qualified Text.StringTemplate as T
+import Network.HTTP (urlEncodeVars)
+import Codec.Binary.UTF8.String (encodeString) 
+
+data ValidationType = Register
+                    | ResetPassword
+                    deriving (Show,Read)
+
+registerUser :: Params -> Handler
+registerUser params = do
+  result' <- sharedValidation Register params
+  case result' of
+    Left errors -> registerForm >>=
+          formattedPage defaultPageLayout{
+                          pgMessages = errors,
+                          pgShowPageTools = False,
+                          pgTabs = [],
+                          pgTitle = "Register for an account"
+                          }
+    Right (uname, email, pword) -> do
+       user <- liftIO $ mkUser uname email pword
+       addUser uname user
+       loginUser params{ pUsername = uname,
+                         pPassword = pword,
+                         pEmail = email }
+
+resetPasswordRequestForm :: Params -> Handler
+resetPasswordRequestForm _ = do
+  let passwordForm = gui "" ! [identifier "resetPassword"] << fieldset <<
+              [ label << "Username: "
+              , textfield "username" ! [size "20", intAttr "tabindex" 1], stringToHtml " "
+              , submit "resetPassword" "Reset Password" ! [intAttr "tabindex" 2]]
+  cfg <- getConfig
+  let contents = if null (mailCommand cfg)
+                    then p << "Sorry, password reset not available."
+                    else passwordForm
+  formattedPage defaultPageLayout{
+                  pgShowPageTools = False,
+                  pgTabs = [],
+                  pgTitle = "Reset your password" }
+                contents
+
+resetPasswordRequest :: Params -> Handler
+resetPasswordRequest params = do
+  let uname = pUsername params
+  mbUser <- getUser uname
+  let errors = case mbUser of
+        Nothing -> ["Unknown user. Please re-register " ++
+                    "or press the Back button to try again."]
+        Just u  -> ["Since you did not register with " ++
+                   "an email address, we can't reset your password." |
+                    null (uEmail u) ]
+  if null errors
+    then do
+      let response =
+            p << [ stringToHtml "An email has been sent to "
+                 , bold $ stringToHtml . uEmail $ fromJust mbUser
+                 , br
+                 , stringToHtml
+                   "Please click on the enclosed link to reset your password."
+                 ]
+      sendReregisterEmail (fromJust mbUser)
+      formattedPage defaultPageLayout{
+                      pgShowPageTools = False,
+                      pgTabs = [],
+                      pgTitle = "Resetting your password"
+                      }
+                    response
+    else registerForm >>=
+         formattedPage defaultPageLayout{
+                         pgMessages = errors, 
+                         pgShowPageTools = False,
+                         pgTabs = [],
+                         pgTitle = "Register for an account"
+                         }
+
+resetLink :: String -> User -> String
+resetLink base' user =
+  exportURL $  foldl add_param
+    (fromJust . importURL $ base' ++ "/_doResetPassword")
+    [("username", uUsername user), ("reset_code", take 20 (pHashed (uPassword user)))]
+
+sendReregisterEmail :: User -> GititServerPart ()
+sendReregisterEmail user = do
+  cfg <- getConfig
+  hostname <- liftIO getHostName
+  base' <- getWikiBase
+  let messageTemplate = T.newSTMP $ resetPasswordMessage cfg
+  let filledTemplate = T.render .
+                       T.setAttribute "username" (uUsername user) .
+                       T.setAttribute "useremail" (uEmail user) .
+                       T.setAttribute "hostname" hostname .
+                       T.setAttribute "port" (show $ portNumber cfg) .
+                       T.setAttribute "resetlink" (resetLink base' user) $
+                       messageTemplate
+  let (mailcommand:args) = words $ substitute "%s" (uEmail user)
+                                   (mailCommand cfg)
+  (exitCode, _pOut, pErr) <- liftIO $ readProcessWithExitCode mailcommand args
+                                      filledTemplate
+  liftIO $ logM "gitit" WARNING $ "Sent reset password email to " ++ uUsername user ++
+                         " at " ++ uEmail user
+  unless (exitCode == ExitSuccess) $
+    liftIO $ logM "gitit" WARNING $ mailcommand ++ " failed. " ++ pErr
+
+validateReset :: Params -> (User -> Handler) -> Handler
+validateReset params postValidate = do
+  let uname = pUsername params
+  user <- getUser uname
+  let knownUser = isJust user
+  let resetCodeMatches = take 20 (pHashed (uPassword (fromJust user))) ==
+                           pResetCode params
+  let errors = case (knownUser, resetCodeMatches) of
+                     (True, True)   -> []
+                     (True, False)  -> ["Your reset code is invalid"]
+                     (False, _)     -> ["User " ++ uname ++ " is not known"] 
+  if null errors
+     then postValidate (fromJust user)
+     else registerForm >>=
+          formattedPage defaultPageLayout{
+                          pgMessages = errors,
+                          pgShowPageTools = False,
+                          pgTabs = [],
+                          pgTitle = "Register for an account"
+                          }
+
+resetPassword :: Params -> Handler
+resetPassword params = validateReset params $ \user ->
+  resetPasswordForm (Just user) >>=
+  formattedPage defaultPageLayout{
+                  pgShowPageTools = False,
+                  pgTabs = [],
+                  pgTitle = "Reset your registration info"
+                  }
+
+doResetPassword :: Params -> Handler
+doResetPassword params = validateReset params $ \user -> do
+  result' <- sharedValidation ResetPassword params
+  case result' of
+    Left errors ->
+      resetPasswordForm (Just user) >>=
+          formattedPage defaultPageLayout{
+                          pgMessages = errors,
+                          pgShowPageTools = False,
+                          pgTabs = [],
+                          pgTitle = "Reset your registration info"
+                          }
+    Right (uname, email, pword) -> do
+       user' <- liftIO $ mkUser uname email pword
+       adjustUser uname user'
+       liftIO $ logM "gitit" WARNING $
+            "Successfully reset password and email for " ++ uUsername user'
+       loginUser params{ pUsername = uname,
+                         pPassword = pword,
+                         pEmail = email }
+
+registerForm :: GititServerPart Html
+registerForm = sharedForm Nothing
+
+resetPasswordForm :: Maybe User -> GititServerPart Html
+resetPasswordForm = sharedForm  -- synonym for now
+
+sharedForm :: Maybe User -> GititServerPart Html
+sharedForm mbUser = withData $ \params -> do
+  cfg <- getConfig
+  dest <- case pDestination params of
+                ""  -> getReferer
+                x   -> return x
+  let accessQ = case accessQuestion cfg of
+                      Nothing          -> noHtml
+                      Just (prompt, _) -> label << prompt +++ br +++
+                                          X.password "accessCode" ! [size "15", intAttr "tabindex" 1]
+                                          +++ br
+  let captcha = if useRecaptcha cfg
+                   then captchaFields (recaptchaPublicKey cfg) Nothing
+                   else noHtml
+  let initField field = case mbUser of
+                      Nothing    -> ""
+                      Just user  -> field user
+  let userNameField = case mbUser of
+                      Nothing    -> label <<
+                                     "Username (at least 3 letters or digits):"
+                                    +++ br +++
+                                    textfield "username" ! [size "20", intAttr "tabindex" 2] +++ br
+                      Just user  -> label << ("Username (cannot be changed): "
+                                               ++ uUsername user) +++ br
+  let submitField = case mbUser of
+                      Nothing    -> submit "register" "Register"
+                      Just _     -> submit "resetPassword" "Reset Password"
+
+  return $ gui "" ! [identifier "loginForm"] << fieldset <<
+            [ accessQ
+            , userNameField
+            , label << "Email (optional, will not be displayed on the Wiki):"
+            , br
+            , textfield "email" ! [size "20", intAttr "tabindex" 3, value (initField uEmail)], br
+            , textfield "full_name_1" ! [size "20", theclass "req"]
+            , label << ("Password (at least 6 characters," ++
+                        " including at least one non-letter):")
+            , br
+            , X.password "password" ! [size "20", intAttr "tabindex" 4]
+            , stringToHtml " "
+            , br
+            , label << "Confirm Password:"
+            , br
+            , X.password "password2" ! [size "20", intAttr "tabindex" 5]
+            , stringToHtml " "
+            , br
+            , captcha
+            , textfield "destination" ! [thestyle "display: none;", value dest]
+            , submitField ! [intAttr "tabindex" 6]]
+
+
+sharedValidation :: ValidationType
+                 -> Params
+                 -> GititServerPart (Either [String] (String,String,String))
+sharedValidation validationType params = do
+  let isValidUsername u = length u >= 3 && all isAlphaNum u
+  let isValidPassword pw = length pw >= 6 && not (all isAlpha pw)
+  let accessCode = pAccessCode params
+  let uname = pUsername params
+  let pword = pPassword params
+  let pword2 = pPassword2 params
+  let email = pEmail params
+  let fakeField = pFullName params
+  let recaptcha = pRecaptcha params
+  taken <- isUser uname
+  cfg <- getConfig
+  let optionalTests Register =
+          [(taken, "Sorry, that username is already taken.")]
+      optionalTests ResetPassword = []
+  let isValidAccessCode = case accessQuestion cfg of
+        Nothing           -> True
+        Just (_, answers) -> accessCode `elem` answers
+  let isValidEmail e = length (filter (=='@') e) == 1
+  peer <- liftM (fst . rqPeer) askRq
+  captchaResult <-
+    if useRecaptcha cfg
+       then if null (recaptchaChallengeField recaptcha) ||
+                 null (recaptchaResponseField recaptcha)
+               -- no need to bother captcha.net in this case
+               then return $ Left "missing-challenge-or-response"
+               else liftIO $ do
+                      mbIPaddr <- lookupIPAddr peer
+                      let ipaddr = case mbIPaddr of
+                                        Just ip -> ip
+                                        Nothing -> error $
+                                          "Could not find ip address for " ++
+                                          peer
+                      ipaddr `seq` validateCaptcha (recaptchaPrivateKey cfg)
+                              ipaddr (recaptchaChallengeField recaptcha)
+                              (recaptchaResponseField recaptcha)
+       else return $ Right ()
+  let (validCaptcha, captchaError) =
+        case captchaResult of
+              Right () -> (True, Nothing)
+              Left err -> (False, Just err)
+  let errors = validate $ optionalTests validationType ++
+        [ (not isValidAccessCode, "Incorrect response to access prompt.")
+        , (not (isValidUsername uname),
+         "Username must be at least 3 charcaters, all letters or digits.")
+        , (not (isValidPassword pword),
+         "Password must be at least 6 characters, " ++
+         "and must contain at least one non-letter.")
+        , (not (null email) && not (isValidEmail email),
+         "Email address appears invalid.")
+        , (pword /= pword2,
+        "Password does not match confirmation.")
+        , (not validCaptcha,
+        "Failed CAPTCHA (" ++ fromJust captchaError ++
+        "). Are you really human?")
+        , (not (null fakeField), -- fakeField is hidden in CSS (honeypot)
+        "You do not seem human enough. If you're sure you are human, " ++
+        "try turning off form auto-completion in your browser.")
+        ]
+  return $ if null errors then Right (uname, email, pword) else Left errors
+
+-- user authentication
+loginForm :: String -> GititServerPart Html
+loginForm dest = do
+  cfg <- getConfig
+  base' <- getWikiBase
+  return $ gui (base' ++ "/_login") ! [identifier "loginForm"] <<
+    fieldset <<
+      [ label << "Username "
+      , textfield "username" ! [size "15", intAttr "tabindex" 1]
+      , stringToHtml " "
+      , label << "Password "
+      , X.password "password" ! [size "15", intAttr "tabindex" 2]
+      , stringToHtml " "
+      , textfield "destination" ! [thestyle "display: none;", value dest]
+      , submit "login" "Login" ! [intAttr "tabindex" 3]
+      ] +++
+    p << [ stringToHtml "If you do not have an account, "
+         , anchor ! [href $ base' ++ "/_register?" ++
+           urlEncodeVars [("destination", encodeString dest)]] << "click here to get one."
+         ] +++
+    if null (mailCommand cfg)
+       then noHtml
+       else p << [ stringToHtml "If you forgot your password, "
+                 , anchor ! [href $ base' ++ "/_resetPassword"] <<
+                     "click here to get a new one."
+                 ]
+
+loginUserForm :: Handler
+loginUserForm = withData $ \params -> do
+  dest <- case pDestination params of
+                ""  -> getReferer
+                x   -> return x
+  loginForm dest >>=
+    formattedPage defaultPageLayout{ pgShowPageTools = False,
+                                     pgTabs = [],
+                                     pgTitle = "Login",
+                                     pgMessages = pMessages params
+                                   }
+
+loginUser :: Params -> Handler
+loginUser params = do
+  let uname = pUsername params
+  let pword = pPassword params
+  let destination = pDestination params
+  allowed <- authUser uname pword
+  if allowed
+    then do
+      key <- newSession (SessionData uname)
+      addCookie sessionTime (mkCookie "sid" (show key))
+      seeOther (encUrl destination) $ toResponse $ p << ("Welcome, " ++ uname)
+    else
+      loginUserForm
+
+encUrl :: String -> String
+encUrl = encString True isAscii
+
+logoutUser :: Params -> Handler
+logoutUser params = do
+  let key = pSessionKey params
+  dest <- case pDestination params of
+                ""  -> getReferer
+                x   -> return x
+  case key of
+       Just k  -> do
+         delSession k
+         -- make cookie expire immediately, effectively deleting it
+         addCookie 0 (mkCookie "sid" "-1")
+       Nothing -> return ()
+  seeOther (encUrl dest) $ toResponse "You have been logged out."
+
+registerUserForm :: Handler
+registerUserForm = registerForm >>=
+    formattedPage defaultPageLayout{
+                    pgShowPageTools = False,
+                    pgTabs = [],
+                    pgTitle = "Register for an account"
+                    }
+
+formAuthHandlers :: [Handler]
+formAuthHandlers =
+  [ dir "_register"  $ methodSP GET  registerUserForm
+  , dir "_register"  $ methodSP POST $ withData registerUser
+  , dir "_login"     $ methodSP GET  loginUserForm
+  , dir "_login"     $ methodSP POST $ withData loginUser
+  , dir "_logout"    $ methodSP GET  $ withData logoutUser
+  , dir "_resetPassword"   $ methodSP GET  $ withData resetPasswordRequestForm
+  , dir "_resetPassword"   $ methodSP POST $ withData resetPasswordRequest
+  , dir "_doResetPassword" $ methodSP GET  $ withData resetPassword
+  , dir "_doResetPassword" $ methodSP POST $ withData doResetPassword
+  ]
+
+loginUserHTTP :: Params -> Handler
+loginUserHTTP params = do
+  base' <- getWikiBase
+  let destination = pDestination params `orIfNull` (base' ++ "/")
+  seeOther (encUrl destination) $ toResponse ()
+
+logoutUserHTTP :: Handler
+logoutUserHTTP = unauthorized $ toResponse ()  -- will this work?
+
+httpAuthHandlers :: [Handler]
+httpAuthHandlers =
+  [ dir "_logout" $ logoutUserHTTP
+  , dir "_login"  $ withData loginUserHTTP ]
diff --git a/Network/Gitit/Cache.hs b/Network/Gitit/Cache.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Cache.hs
@@ -0,0 +1,66 @@
+{-
+Copyright (C) 2008 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- Functions for maintaining user list and session state.
+-}
+
+module Network.Gitit.Cache ( expireCachedFile
+                           , lookupCache
+                           , cacheContents )
+where
+
+import qualified Data.ByteString as B (ByteString, readFile, writeFile)
+import System.FilePath
+import System.Directory (doesFileExist, removeFile, createDirectoryIfMissing, getModificationTime)
+import System.Time (ClockTime)
+import Network.Gitit.State
+import Network.Gitit.Types
+import Control.Monad
+import Control.Monad.Trans (liftIO)
+
+-- | Expire a cached file, identified by its filename in the filestore.
+-- Returns () after deleting a file from the cache, fails if no cached file.
+expireCachedFile :: String -> GititServerPart ()
+expireCachedFile file = do
+  cfg <- getConfig
+  let target = cacheDir cfg </> file
+  exists <- liftIO $ doesFileExist target
+  if exists
+     then liftIO (removeFile target)
+     else mzero
+
+lookupCache :: String -> GititServerPart (Maybe (ClockTime, B.ByteString))
+lookupCache file = do
+  cfg <- getConfig
+  let target = cacheDir cfg </> file
+  exists <- liftIO $ doesFileExist target
+  if exists
+     then liftIO $ do
+       modtime <- getModificationTime target
+       contents <- B.readFile target
+       return $ Just (modtime, contents)
+     else return Nothing
+
+cacheContents :: String -> B.ByteString -> GititServerPart ()
+cacheContents file contents = do
+  cfg <- getConfig
+  let target = cacheDir cfg </> file
+  let targetDir = takeDirectory target
+  liftIO $ do
+    createDirectoryIfMissing True targetDir
+    B.writeFile (cacheDir cfg </> file) contents
diff --git a/Network/Gitit/Config.hs b/Network/Gitit/Config.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Config.hs
@@ -0,0 +1,317 @@
+{-# LANGUAGE CPP, FlexibleContexts #-}
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- | Functions for parsing command line options and reading the config file.
+-}
+
+module Network.Gitit.Config ( getConfigFromOpts
+                            , readMimeTypesFile
+                            , getDefaultConfig )
+where
+import Network.Gitit.Types
+import Network.Gitit.Server (mimeTypes)
+import Network.Gitit.Framework
+import Network.Gitit.Authentication (formAuthHandlers, httpAuthHandlers)
+import Network.Gitit.Util (parsePageType)
+import System.Log.Logger (logM, Priority(..))
+import qualified Data.Map as M
+import System.Environment
+import System.Exit
+import System.IO (stdout, stderr)
+import System.Console.GetOpt
+import Data.ConfigFile
+import Control.Monad.Error
+import System.Log.Logger ()
+import Data.List (intercalate)
+import Data.Char (toLower, toUpper, isDigit)
+import Data.Version (showVersion)
+import Paths_gitit (getDataFileName, version)
+import Prelude hiding (readFile)
+import System.IO.UTF8
+import System.FilePath ((</>))
+import Control.Monad (liftM)
+import Text.Pandoc
+
+data Opt
+    = Help
+    | ConfigFile FilePath
+    | Port Int
+    | Debug
+    | Version
+    | PrintDefaultConfig
+    deriving (Eq)
+
+flags :: [OptDescr Opt]
+flags =
+   [ Option ['h'] ["help"] (NoArg Help)
+        "Print this help message"
+   , Option ['v'] ["version"] (NoArg Version)
+        "Print version information"
+   , Option ['p'] ["port"] (ReqArg (Port . read) "PORT")
+        "Specify port"
+   , Option [] ["print-default-config"] (NoArg PrintDefaultConfig)
+        "Print default configuration"
+   , Option ['d'] ["debug"] (NoArg Debug)
+        "Print debugging information on each request"
+   , Option ['f'] ["config-file"] (ReqArg ConfigFile "FILE")
+        "Specify configuration file"
+   ]
+
+parseArgs :: [String] -> IO [Opt]
+parseArgs argv = do
+  progname <- getProgName
+  case getOpt Permute flags argv of
+    (opts,_,[])  -> return opts
+    (_,_,errs)   -> hPutStrLn stderr (concat errs ++ usageInfo (usageHeader progname) flags) >>
+                       exitWith (ExitFailure 1)
+
+usageHeader :: String -> String
+usageHeader progname = "Usage:  " ++ progname ++ " [opts...]"
+
+copyrightMessage :: String
+copyrightMessage = "\nCopyright (C) 2008 John MacFarlane\n" ++
+                   "This is free software; see the source for copying conditions.  There is no\n" ++
+                   "warranty, not even for merchantability or fitness for a particular purpose."
+
+compileInfo :: String
+compileInfo =
+#ifdef _PLUGINS
+  " +plugins"
+#else
+  " -plugins"
+#endif
+
+forceEither :: Show e => Either e a -> a
+forceEither = either (error . show) id
+
+handleFlag :: ConfigParser -> Config -> Opt -> IO Config
+handleFlag cp conf opt = do
+  progname <- getProgName
+  case opt of
+    Help               -> hPutStrLn stderr (usageInfo (usageHeader progname) flags) >> exitWith ExitSuccess
+    Version            -> hPutStrLn stderr (progname ++ " version " ++ showVersion version ++ compileInfo ++ copyrightMessage) >> exitWith ExitSuccess
+    PrintDefaultConfig -> getDataFileName "data/default.conf" >>= readFile >>=
+                          hPutStrLn stdout >> exitWith ExitSuccess
+    Debug              -> return conf{ debugMode = True }
+    Port p             -> return conf{ portNumber = p }
+    ConfigFile fname   -> readfile cp fname >>= extractConfig . forceEither
+
+extractConfig :: ConfigParser -> IO Config
+extractConfig cp = do
+  config' <- runErrorT $ do
+      cfRepositoryType <- get cp "DEFAULT" "repository-type"
+      cfRepositoryPath <- get cp "DEFAULT" "repository-path"
+      cfDefaultPageType <- get cp "DEFAULT" "default-page-type"
+      cfMathMethod <- get cp "DEFAULT" "math"
+      cfShowLHSBirdTracks <- get cp "DEFAULT" "show-lhs-bird-tracks"
+      cfAuthenticationMethod <- get cp "DEFAULT" "authentication-method"
+      cfUserFile <- get cp "DEFAULT" "user-file"
+      cfTemplatesDir <- get cp "DEFAULT" "templates-dir"
+      cfLogFile <- get cp "DEFAULT" "log-file"
+      cfLogLevel <- get cp "DEFAULT" "log-level"
+      cfStaticDir <- get cp "DEFAULT" "static-dir"
+      cfPlugins <- get cp "DEFAULT" "plugins"
+      cfTableOfContents <- get cp "DEFAULT" "table-of-contents"
+      cfMaxUploadSize <- get cp "DEFAULT" "max-upload-size"
+      cfPort <- get cp "DEFAULT" "port"
+      cfDebugMode <- get cp "DEFAULT" "debug-mode"
+      cfFrontPage <- get cp "DEFAULT" "front-page"
+      cfNoEdit <- get cp "DEFAULT" "no-edit"
+      cfNoDelete <- get cp "DEFAULT" "no-delete"
+      cfDefaultSummary <- get cp "DEFAULT" "default-summary"
+      cfAccessQuestion <- get cp "DEFAULT" "access-question"
+      cfAccessQuestionAnswers <- get cp "DEFAULT" "access-question-answers"
+      cfUseRecaptcha <- get cp "DEFAULT" "use-recaptcha"
+      cfRecaptchaPublicKey <- get cp "DEFAULT" "recaptcha-public-key"
+      cfRecaptchaPrivateKey <- get cp "DEFAULT" "recaptcha-private-key"
+      cfCompressResponses <- get cp "DEFAULT" "compress-responses"
+      cfUseCache <- get cp "DEFAULT" "use-cache"
+      cfCacheDir <- get cp "DEFAULT" "cache-dir"
+      cfMimeTypesFile <- get cp "DEFAULT" "mime-types-file"
+      cfMailCommand <- get cp "DEFAULT" "mail-command"
+      cfResetPasswordMessage <- get cp "DEFAULT" "reset-password-message"
+      cfUseFeed <- get cp "DEFAULT" "use-feed"
+      cfBaseUrl <- get cp "DEFAULT" "base-url"
+      cfWikiTitle <- get cp "DEFAULT" "wiki-title"
+      cfFeedDays <- get cp "DEFAULT" "feed-days"
+      cfFeedRefreshTime <- get cp "DEFAULT" "feed-refresh-time"
+      let (pt, lhs) = parsePageType cfDefaultPageType
+      let markupHelpFile = show pt ++ if lhs then "+LHS" else ""
+      markupHelpPath <- liftIO $ getDataFileName $ "data" </> "markupHelp" </> markupHelpFile
+      markupHelpText <- liftM (writeHtmlString defaultWriterOptions . readMarkdown defaultParserState) $
+                            liftIO $ readFile markupHelpPath
+
+      mimeMap' <- liftIO $ readMimeTypesFile cfMimeTypesFile
+      let authMethod = map toLower cfAuthenticationMethod
+      let stripTrailingSlash = reverse . dropWhile (=='/') . reverse
+      let repotype' = case map toLower cfRepositoryType of
+                        "git"   -> Git
+                        "darcs" -> Darcs
+                        x       -> error $ "Unknown repository type: " ++ x
+
+      return $! Config{
+          repositoryPath       = cfRepositoryPath
+        , repositoryType       = repotype'
+        , defaultPageType      = pt
+        , mathMethod           = case map toLower cfMathMethod of
+                                      "jsmath"   -> JsMathScript
+                                      "mathml"   -> MathML
+                                      _          -> RawTeX
+        , defaultLHS           = lhs
+        , showLHSBirdTracks    = cfShowLHSBirdTracks
+        , withUser             = case authMethod of
+                                      "form"     -> withUserFromSession
+                                      "http"     -> withUserFromHTTPAuth
+                                      _          -> id
+        , authHandler          = case authMethod of
+                                      "form"     -> msum formAuthHandlers
+                                      "http"     -> msum httpAuthHandlers
+                                      _          -> mzero
+        , userFile             = cfUserFile
+        , templatesDir         = cfTemplatesDir
+        , logFile              = cfLogFile
+        , logLevel             = let levelString = map toUpper cfLogLevel
+                                     levels = ["DEBUG", "INFO", "NOTICE", "WARNING", "ERROR",
+                                               "CRITICAL", "ALERT", "EMERGENCY"]
+                                 in  if levelString `elem` levels
+                                        then read levelString
+                                        else error $ "Invalid log-level.\nLegal values are: " ++ intercalate ", " levels
+        , staticDir            = cfStaticDir
+        , pluginModules        = splitCommaList cfPlugins
+        , tableOfContents      = cfTableOfContents
+        , maxUploadSize        = readNumber "max-upload-size" cfMaxUploadSize
+        , portNumber           = readNumber "port" cfPort
+        , debugMode            = cfDebugMode
+        , frontPage            = cfFrontPage
+        , noEdit               = splitCommaList cfNoEdit
+        , noDelete             = splitCommaList cfNoDelete
+        , defaultSummary       = cfDefaultSummary
+        , accessQuestion       = if null cfAccessQuestion
+                                    then Nothing
+                                    else Just (cfAccessQuestion, splitCommaList cfAccessQuestionAnswers)
+        , useRecaptcha         = cfUseRecaptcha
+        , recaptchaPublicKey   = cfRecaptchaPublicKey
+        , recaptchaPrivateKey  = cfRecaptchaPrivateKey
+        , compressResponses    = cfCompressResponses
+        , useCache             = cfUseCache
+        , cacheDir             = cfCacheDir
+        , mimeMap              = mimeMap'
+        , mailCommand          = cfMailCommand
+        , resetPasswordMessage = fromQuotedMultiline cfResetPasswordMessage
+        , markupHelp           = markupHelpText
+        , useFeed              = cfUseFeed
+        , baseUrl              = stripTrailingSlash cfBaseUrl
+        , wikiTitle            = cfWikiTitle
+        , feedDays             = readNumber "feed-days" cfFeedDays
+        , feedRefreshTime      = readNumber "feed-refresh-time" cfFeedRefreshTime }
+  case config' of
+        Left (ParseError e, e') -> error $ "Parse error: " ++ e ++ "\n" ++ e'
+        Left e                  -> error (show e)
+        Right c                 -> return c
+
+fromQuotedMultiline :: String -> String
+fromQuotedMultiline = unlines . map doline . lines . dropWhile (`elem` " \t\n")
+  where doline = dropWhile (`elem` " \t") . dropGt
+        dropGt ('>':' ':xs) = xs
+        dropGt ('>':xs) = xs
+        dropGt x = x
+
+readNumber :: (Read a) => String -> String -> a
+readNumber opt "" = error $ opt ++ " must be a number."
+readNumber opt x  =
+  let x' = case last x of
+                'K'  -> init x ++ "000"
+                'M'  -> init x ++ "000000"
+                'G'  -> init x ++ "000000000"
+                _    -> x
+  in if all isDigit x'
+        then read x'
+        else error $ opt ++ " must be a number."
+
+splitCommaList :: String -> [String]
+splitCommaList l =
+  let (first,rest) = break (== ',') l
+      first' = lrStrip first
+  in  if null rest
+         then if null first' then [] else [first']
+         else first' : splitCommaList (tail rest)
+
+lrStrip :: String -> String
+lrStrip = reverse . dropWhile isWhitespace . reverse . dropWhile isWhitespace
+    where isWhitespace = (`elem` " \t\n")
+
+getDefaultConfigParser :: IO ConfigParser
+getDefaultConfigParser = do
+  cp <- getDataFileName "data/default.conf" >>= readfile emptyCP
+  return $ forceEither cp
+
+-- | Returns the default gitit configuration.
+getDefaultConfig :: IO Config
+getDefaultConfig = getDefaultConfigParser >>= extractConfig
+
+-- | Parses command line options and returns configuration
+-- based on the options (-f FILE specifies a configuration
+-- file; some settings, such as port number, can be overridden
+-- by a command line option).
+getConfigFromOpts :: IO Config
+getConfigFromOpts = do
+  cp' <- getDefaultConfigParser
+  defaultConfig <- extractConfig cp'
+  getArgs >>= parseArgs >>= foldM (handleFlag cp') defaultConfig
+
+-- | Read a file associating mime types with extensions, and return a
+-- map from extensions to types. Each line of the file consists of a
+-- mime type, followed by space, followed by a list of zero or more
+-- extensions, separated by spaces. Example: text/plain txt text
+readMimeTypesFile :: FilePath -> IO (M.Map String String)
+readMimeTypesFile f = catch
+  (liftM (foldr go M.empty . map words . lines) $ readFile f)
+  handleMimeTypesFileNotFound
+     where go []     m = m  -- skip blank lines
+           go (x:xs) m = foldr (\ext -> M.insert ext x) m xs
+           handleMimeTypesFileNotFound e = do
+             logM "gitit" WARNING $ "Could not read mime types file: " ++
+               f ++ "\n" ++ show e ++ "\n" ++ "Using defaults instead."
+             return mimeTypes
+
+{-
+-- | Ready collection of common mime types. (Copied from
+-- Happstack.Server.HTTP.FileServe.)
+mimeTypes :: M.Map String String
+mimeTypes = M.fromList
+        [("xml","application/xml")
+        ,("xsl","application/xml")
+        ,("js","text/javascript")
+        ,("html","text/html")
+        ,("htm","text/html")
+        ,("css","text/css")
+        ,("gif","image/gif")
+        ,("jpg","image/jpeg")
+        ,("png","image/png")
+        ,("txt","text/plain")
+        ,("doc","application/msword")
+        ,("exe","application/octet-stream")
+        ,("pdf","application/pdf")
+        ,("zip","application/zip")
+        ,("gz","application/x-gzip")
+        ,("ps","application/postscript")
+        ,("rtf","application/rtf")
+        ,("wav","application/x-wav")
+        ,("hs","text/plain")]
+-}
diff --git a/Network/Gitit/ContentTransformer.hs b/Network/Gitit/ContentTransformer.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/ContentTransformer.hs
@@ -0,0 +1,555 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>,
+Anton van Straaten <anton@appsolutions.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- Functions for content conversion.
+-}
+
+module Network.Gitit.ContentTransformer
+  (
+  -- * ContentTransformer runners
+    runPageTransformer
+  , runFileTransformer
+  -- * Gitit responders
+  , showRawPage
+  , showFileAsText
+  , showPage
+  , exportPage
+  , showHighlightedSource
+  , showFile
+  , preview
+  , applyPreCommitPlugins
+  -- * Cache support for transformers
+  , cacheHtml
+  , cachedHtml
+  -- * Content retrieval combinators
+  , rawContents
+  -- * Response-generating combinators
+  , textResponse
+  , mimeFileResponse
+  , mimeResponse
+  , exportPandoc
+  , applyWikiTemplate
+  -- * Content-type transformation combinators
+  , pageToWikiPandoc
+  , pageToPandoc
+  , pandocToHtml
+  , highlightSource
+  -- * Content or context augmentation combinators
+  , applyPageTransforms
+  , wikiDivify
+  , addPageTitleToPandoc
+  , addMathSupport
+  , addScripts
+  -- * ContentTransformer context API
+  , getFileName
+  , getPageName
+  , getLayout
+  , getParams
+  , getCacheable
+  -- * Pandoc and wiki content conversion support
+  , inlinesToURL
+  , inlinesToString
+  )
+where
+
+import Prelude hiding (catch)
+import Network.Gitit.Server
+import Network.Gitit.Framework
+import Network.Gitit.State
+import Network.Gitit.Types
+import Network.Gitit.Layout
+import Network.Gitit.Export (exportFormats)
+import Network.Gitit.Page (stringToPage)
+import Network.Gitit.Cache (lookupCache, cacheContents)
+import qualified Data.FileStore as FS
+import Data.Maybe (mapMaybe)
+import Text.Pandoc
+import Text.Pandoc.Shared (HTMLMathMethod(..))
+import Text.XHtml hiding ( (</>), dir, method, password, rev )
+import Text.Highlighting.Kate
+import Data.Maybe (isNothing)
+import Codec.Binary.UTF8.String (encodeString)
+import System.FilePath
+import Control.Monad.State
+import Control.Exception (throwIO, catch)
+import Network.URI (isAllowedInURI, escapeURIString)
+import qualified Data.ByteString as S (concat) 
+import qualified Data.ByteString.Lazy as L (toChunks, fromChunks)
+import Text.XML.Light
+import Text.TeXMath
+
+--
+-- ContentTransformer runners
+--
+
+runTransformer :: ToMessage a
+               => (String -> String)
+               -> ContentTransformer a
+               -> GititServerPart a 
+runTransformer pathFor xform = withData $ \params -> do
+  page <- getPage
+  cfg <- getConfig
+  evalStateT xform  Context{ ctxFile = pathFor page
+                           , ctxLayout = defaultPageLayout{
+                                             pgPageName = page
+                                           , pgTitle = page
+                                           , pgPrintable = pPrintable params
+                                           , pgMessages = pMessages params
+                                           , pgRevision = pRevision params
+                                           , pgLinkToFeed = useFeed cfg }
+                           , ctxCacheable = True
+                           , ctxTOC = tableOfContents cfg
+                           , ctxBirdTracks = showLHSBirdTracks cfg
+                           , ctxCategories = [] }
+
+-- | Converts a @ContentTransformer@ into a @GititServerPart@;
+-- specialized to wiki pages.
+runPageTransformer :: ToMessage a
+                   => ContentTransformer a
+                   -> GititServerPart a 
+runPageTransformer = runTransformer pathForPage
+
+-- | Converts a @ContentTransformer@ into a @GititServerPart@;
+-- specialized to non-pages.
+runFileTransformer :: ToMessage a
+                   => ContentTransformer a
+                   -> GititServerPart a
+runFileTransformer = runTransformer id
+
+--
+-- Gitit responders
+--
+
+-- | Responds with raw page source.
+showRawPage :: Handler
+showRawPage = runPageTransformer rawTextResponse
+
+-- | Responds with raw source (for non-pages such as source
+-- code files).
+showFileAsText :: Handler
+showFileAsText = runFileTransformer rawTextResponse
+
+-- | Responds with rendered wiki page.
+showPage :: Handler
+showPage = runPageTransformer htmlViaPandoc
+
+-- | Responds with page exported into selected format.
+exportPage :: Handler 
+exportPage = runPageTransformer exportViaPandoc
+
+-- | Responds with highlighted source code.
+showHighlightedSource :: Handler
+showHighlightedSource = runFileTransformer highlightRawSource
+
+-- | Responds with non-highlighted source code.
+showFile :: Handler
+showFile = runFileTransformer (rawContents >>= mimeFileResponse)
+
+-- | Responds with rendered page derived from form data.
+preview :: Handler
+preview = runPageTransformer $
+          liftM (filter (/= '\r') . pRaw) getParams >>=
+          contentsToPage >>=
+          pageToWikiPandoc >>=
+          pandocToHtml >>=
+          return . toResponse . renderHtmlFragment
+
+-- | Applies pre-commit plugins to raw page source, possibly
+-- modifying it.
+applyPreCommitPlugins :: String -> GititServerPart String
+applyPreCommitPlugins = runPageTransformer . applyPreCommitTransforms
+
+--
+-- Top level, composed transformers
+--
+
+-- | Responds with raw source.
+rawTextResponse :: ContentTransformer Response
+rawTextResponse = rawContents >>= textResponse
+
+-- | Responds with a wiki page in the format specified
+-- by the @format@ parameter. 
+exportViaPandoc :: ContentTransformer Response
+exportViaPandoc = rawContents >>=
+                  maybe mzero return >>=
+                  contentsToPage >>=
+                  pageToWikiPandoc >>=
+                  exportPandoc
+
+-- | Responds with a wiki page. Uses the cache when
+-- possible and caches the rendered page when appropriate.
+htmlViaPandoc :: ContentTransformer Response
+htmlViaPandoc = cachedHtml `mplus`
+                  (rawContents >>=
+                   maybe mzero return >>=
+                   contentsToPage >>=
+                   pageToWikiPandoc >>=
+                   addMathSupport >>=
+                   pandocToHtml >>=
+                   wikiDivify >>=
+                   applyWikiTemplate >>=
+                   cacheHtml)
+
+-- | Responds with highlighted source code in a wiki
+-- page template.  Uses the cache when possible and
+-- caches the rendered page when appropriate.
+highlightRawSource :: ContentTransformer Response
+highlightRawSource =
+  cachedHtml `mplus`
+    (updateLayout (\l -> l { pgTabs = [ViewTab,HistoryTab] }) >> 
+     rawContents >>=
+     highlightSource >>=
+     applyWikiTemplate >>=
+     cacheHtml)
+
+--
+-- Cache support for transformers
+--
+
+-- | Caches a response (actually just the response body) on disk,
+-- unless the context indicates that the page is not cacheable. 
+cacheHtml :: Response -> ContentTransformer Response 
+cacheHtml resp = do
+  params <- getParams
+  file <- getFileName
+  cacheable <- getCacheable
+  cfg <- lift getConfig
+  when (useCache cfg && cacheable && isNothing (pRevision params) && not (pPrintable params)) $
+    lift $ cacheContents file $ S.concat $ L.toChunks $ rsBody resp 
+  return resp 
+
+-- | Returns cached page if available, otherwise mzero.
+cachedHtml :: ContentTransformer Response
+cachedHtml = do
+  file <- getFileName
+  params <- getParams
+  cfg <- lift getConfig
+  if useCache cfg && not (pPrintable params) && isNothing (pRevision params)
+     then do mbCached <- lift $ lookupCache file
+             let emptyResponse = setContentType "text/html; charset=utf-8" . toResponse $ ()
+             maybe mzero (\(_modtime, contents) -> lift . ok $ emptyResponse{rsBody = L.fromChunks [contents]}) mbCached
+     else mzero
+
+--
+-- Content retrieval combinators
+--
+
+-- | Returns raw file contents.
+rawContents :: ContentTransformer (Maybe String)
+rawContents = do
+  params <- getParams
+  file <- getFileName
+  fs <- lift getFileStore
+  let rev = pRevision params
+  liftIO $ catch (liftM Just $ FS.retrieve fs file rev)
+                 (\e -> if e == FS.NotFound then return Nothing else throwIO e)
+
+--
+-- Response-generating combinators
+--
+
+-- | Converts raw contents to a text/plain response.
+textResponse :: Maybe String -> ContentTransformer Response
+textResponse Nothing  = mzero  -- fail quietly if file not found
+textResponse (Just c) = mimeResponse c "text/plain; charset=utf-8"
+
+-- | Converts raw contents to a response that is appropriate with
+-- a mime type derived from the page's extension.
+mimeFileResponse :: Maybe String -> ContentTransformer Response
+mimeFileResponse Nothing = error "Unable to retrieve file contents."
+mimeFileResponse (Just c) =
+  mimeResponse c =<< lift . getMimeTypeForExtension . takeExtension =<< getFileName
+
+mimeResponse :: Monad m
+             => String        -- ^ Raw contents for response body
+             -> String        -- ^ Mime type
+             -> m Response
+mimeResponse c mimeType =
+  return . setContentType mimeType . toResponse $ c
+
+-- | Converts Pandoc to response using format specified in parameters. 
+exportPandoc :: Pandoc -> ContentTransformer Response
+exportPandoc doc = do
+  params <- getParams
+  page <- getPageName
+  let format = pFormat params
+  case lookup format exportFormats of
+       Nothing     -> error $ "Unknown export format: " ++ format
+       Just writer -> lift (writer page doc)
+
+-- | Adds the sidebar, page tabs, and other elements of the wiki page
+-- layout to the raw content.
+applyWikiTemplate :: Html -> ContentTransformer Response
+applyWikiTemplate c = do
+  Context { ctxLayout = layout } <- get
+  lift $ formattedPage layout c
+
+--
+-- Content-type transformation combinators
+--
+
+-- | Converts Page to Pandoc, applies page transforms, and adds page
+-- title.
+pageToWikiPandoc :: Page -> ContentTransformer Pandoc
+pageToWikiPandoc page' =
+  pageToWikiPandoc' page' >>= addPageTitleToPandoc (pageTitle page')
+
+pageToWikiPandoc' :: Page -> ContentTransformer Pandoc
+pageToWikiPandoc' = applyPreParseTransforms >=>
+                     pageToPandoc >=> applyPageTransforms
+
+-- | Converts source text to Pandoc using default page type.
+pageToPandoc :: Page -> ContentTransformer Pandoc
+pageToPandoc page' = do
+  modifyContext $ \ctx -> ctx{ ctxTOC = pageTOC page'
+                             , ctxCategories = pageCategories page' }
+  return $ readerFor (pageFormat page') (pageLHS page') (pageText page')
+
+-- | Converts contents of page file to Page object.
+contentsToPage :: String -> ContentTransformer Page
+contentsToPage s = do
+  cfg <- lift getConfig
+  pn <- getPageName
+  return $ stringToPage cfg pn s
+
+-- | Converts pandoc document to HTML.
+pandocToHtml :: Pandoc -> ContentTransformer Html
+pandocToHtml pandocContents = do
+  base' <- lift getWikiBase
+  toc <- liftM ctxTOC get
+  bird <- liftM ctxBirdTracks get
+  return $ writeHtml defaultWriterOptions{
+                        writerStandalone = False
+                      , writerHTMLMathMethod = JsMath
+                               (Just $ base' ++ "/js/jsMath/easy/load.js")
+                      , writerTableOfContents = toc
+                      , writerLiterateHaskell = bird
+                      } pandocContents
+
+-- | Returns highlighted source code.
+highlightSource :: Maybe String -> ContentTransformer Html
+highlightSource Nothing = mzero
+highlightSource (Just source) = do
+  file <- getFileName
+  let lang' = head $ languagesByExtension $ takeExtension file
+  case highlightAs lang' (filter (/='\r') source) of
+       Left _       -> mzero
+       Right res    -> return $ formatAsXHtml [OptNumberLines] lang' $! res
+
+--
+-- Plugin combinators
+--
+
+getPageTransforms :: ContentTransformer [Pandoc -> PluginM Pandoc]
+getPageTransforms = liftM (mapMaybe pageTransform) $ queryGititState plugins
+  where pageTransform (PageTransform x) = Just x
+        pageTransform _                 = Nothing
+
+getPreParseTransforms :: ContentTransformer [String -> PluginM String]
+getPreParseTransforms = liftM (mapMaybe preParseTransform) $
+                          queryGititState plugins
+  where preParseTransform (PreParseTransform x) = Just x
+        preParseTransform _                     = Nothing
+
+getPreCommitTransforms :: ContentTransformer [String -> PluginM String]
+getPreCommitTransforms = liftM (mapMaybe preCommitTransform) $
+                          queryGititState plugins
+  where preCommitTransform (PreCommitTransform x) = Just x
+        preCommitTransform _                      = Nothing
+
+-- | @applyTransform a t@ applies the transform @t@ to input @a@.
+applyTransform :: a -> (a -> PluginM a) -> ContentTransformer a
+applyTransform inp transform = do
+  context <- get
+  conf <- lift getConfig
+  user <- lift getLoggedInUser
+  fs <- lift getFileStore
+  req <- lift askRq
+  let pluginData = PluginData{ pluginConfig = conf
+                             , pluginUser = user
+                             , pluginRequest = req
+                             , pluginFileStore = fs }
+  (result', context') <- liftIO $ runPluginM (transform inp) pluginData context
+  put context'
+  return result'
+
+-- | Applies all the page transform plugins to a Pandoc document.
+applyPageTransforms :: Pandoc -> ContentTransformer Pandoc 
+applyPageTransforms c = do
+  xforms <- getPageTransforms
+  cfg <- lift getConfig
+  let xforms' = case mathMethod cfg of
+                      MathML -> mathMLTransform : xforms
+                      _      -> xforms
+  foldM applyTransform c (wikiLinksTransform : xforms')
+
+-- | Applies all the pre-parse transform plugins to a Page object.
+applyPreParseTransforms :: Page -> ContentTransformer Page
+applyPreParseTransforms page' = getPreParseTransforms >>= foldM applyTransform (pageText page') >>=
+                                (\t -> return page'{ pageText = t })
+
+-- | Applies all the pre-commit transform plugins to a raw string.
+applyPreCommitTransforms :: String -> ContentTransformer String
+applyPreCommitTransforms c = getPreCommitTransforms >>= foldM applyTransform c
+
+--
+-- Content or context augmentation combinators
+--
+
+-- | Puts rendered page content into a wikipage div, adding
+-- categories.
+wikiDivify :: Html -> ContentTransformer Html
+wikiDivify c = do
+  categories <- liftM ctxCategories get
+  base' <- lift getWikiBase
+  let categoryLink ctg = li (anchor ! [href $ base' ++ "/_category/" ++ ctg] << ctg)
+  let htmlCategories = if null categories
+                          then noHtml
+                          else thediv ! [identifier "categoryList"] << ulist << map categoryLink categories
+  return $ thediv ! [identifier "wikipage"] << [c, htmlCategories]
+
+-- | Adds page title to a Pandoc document.
+addPageTitleToPandoc :: String -> Pandoc -> ContentTransformer Pandoc
+addPageTitleToPandoc title' (Pandoc _ blocks) = do
+  updateLayout $ \layout -> layout{ pgTitle = title' }
+  return $ if null title'
+              then Pandoc (Meta [] [] []) blocks
+              else Pandoc (Meta [Str title'] [] []) blocks
+
+-- | Adds javascript links for math support.
+addMathSupport :: a -> ContentTransformer a
+addMathSupport c = do
+  conf <- lift getConfig
+  updateLayout $ \l ->
+    case mathMethod conf of
+         JsMathScript -> addScripts l ["jsMath/easy/load.js"]
+         -- for MathML, the script is added by mathMLTransform, only
+         -- if the page contains math:
+         MathML       -> l
+         RawTeX       -> l
+  return c
+
+-- | Adds javascripts to page layout.
+addScripts :: PageLayout -> [String] -> PageLayout
+addScripts layout scriptPaths =
+  layout{ pgScripts = scriptPaths ++ pgScripts layout }
+
+--
+-- ContentTransformer context API
+--
+
+getParams :: ContentTransformer Params
+getParams = lift (withData return)
+
+getFileName :: ContentTransformer FilePath
+getFileName = liftM ctxFile get
+
+getPageName :: ContentTransformer String
+getPageName = liftM (pgPageName . ctxLayout) get
+
+getLayout :: ContentTransformer PageLayout
+getLayout = liftM ctxLayout get
+
+getCacheable :: ContentTransformer Bool
+getCacheable = liftM ctxCacheable get
+
+-- | Updates the layout with the result of applying f to the current layout
+updateLayout :: (PageLayout -> PageLayout) -> ContentTransformer ()
+updateLayout f = do
+  ctx <- get
+  let l = ctxLayout ctx
+  put ctx { ctxLayout = f l }
+
+--
+-- Pandoc and wiki content conversion support
+--
+
+readerFor :: PageType -> Bool -> (String -> Pandoc)
+readerFor pt lhs =
+  let defPS = defaultParserState{ stateSanitizeHTML = True
+                                , stateSmart = True
+                                , stateLiterateHaskell = lhs }
+  in case pt of
+       RST      -> readRST defPS
+       Markdown -> readMarkdown defPS
+       LaTeX    -> readLaTeX defPS
+       HTML     -> readHtml defPS
+
+wikiLinksTransform :: Pandoc -> PluginM Pandoc
+wikiLinksTransform = return . processWith convertWikiLinks
+
+-- | Convert links with no URL to wikilinks.
+convertWikiLinks :: Inline -> Inline
+convertWikiLinks (Link ref ("", "")) =
+  Link ref (inlinesToURL ref, "Go to wiki page")
+convertWikiLinks x = x
+
+mathMLTransform :: Pandoc -> PluginM Pandoc
+mathMLTransform inp = do
+  let (Pandoc m blks, mathUsed) = runState (processWithM convertTeXMathToMathML inp) False
+  let scriptLink = RawHtml "<script type=\"text/javascript\" src=\"/js/MathMLinHTML.js\"></script>"
+  let blks' = if mathUsed
+                 then blks ++ [scriptLink]
+                 else blks
+  return $ Pandoc m blks'
+
+-- | Convert math to MathML.  We put this in a Writer monad
+-- to keep track of whether we've actually got any MathML; if not,
+-- we can avoid linking a script.
+convertTeXMathToMathML :: Inline -> State Bool Inline
+convertTeXMathToMathML (Math t x) = do
+  case texMathToMathML t' x of
+       Left _  -> return $ Math t x
+       Right v -> put True >> return (HtmlInline $ ppElement v)
+    where t' = if t == DisplayMath then DisplayBlock else DisplayInline
+convertTeXMathToMathML x = return x
+
+-- | Derives a URL from a list of Pandoc Inline elements.
+inlinesToURL :: [Inline] -> String
+inlinesToURL = escapeURIString isAllowedInURI  . encodeString . inlinesToString
+
+-- | Convert a list of inlines into a string.
+inlinesToString :: [Inline] -> String
+inlinesToString = concatMap go
+  where go x = case x of
+               Str s                   -> s
+               Emph xs                 -> concatMap go xs
+               Strong xs               -> concatMap go xs
+               Strikeout xs            -> concatMap go xs
+               Superscript xs          -> concatMap go xs
+               Subscript xs            -> concatMap go xs
+               SmallCaps xs            -> concatMap go xs
+               Quoted DoubleQuote xs   -> '"' : (concatMap go xs ++ "\"")
+               Quoted SingleQuote xs   -> '\'' : (concatMap go xs ++ "'")
+               Cite _ xs               -> concatMap go xs
+               Code s                  -> s
+               Space                   -> " "
+               EmDash                  -> "---"
+               EnDash                  -> "--"
+               Apostrophe              -> "'"
+               Ellipses                -> "..."
+               LineBreak               -> " "
+               Math DisplayMath s      -> "$$" ++ s ++ "$$"
+               Math InlineMath s       -> "$" ++ s ++ "$"
+               TeX s                   -> s
+               HtmlInline _            -> ""
+               Link xs _               -> concatMap go xs
+               Image xs _              -> concatMap go xs
+               Note _                  -> ""
+
diff --git a/Network/Gitit/Export.hs b/Network/Gitit/Export.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Export.hs
@@ -0,0 +1,109 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- Functions for exporting wiki pages in various formats.
+-}
+
+module Network.Gitit.Export ( exportFormats )
+where
+import Text.Pandoc
+import Text.Pandoc.ODT (saveOpenDocumentAsODT)
+import Network.Gitit.Server
+import Network.Gitit.Util (withTempDir)
+import Network.Gitit.State
+import Network.Gitit.Types
+import Control.Monad.Trans (liftIO)
+import Text.XHtml (noHtml)
+import qualified Data.ByteString.Lazy as B
+import System.FilePath ((<.>), (</>))
+
+defaultRespOptions :: WriterOptions
+defaultRespOptions = defaultWriterOptions { writerStandalone = True
+                                          , writerWrapText = True }
+
+respond :: String
+        -> String
+        -> (Pandoc -> String)
+        -> String
+        -> Pandoc
+        -> Handler
+respond mimetype ext fn page = ok . setContentType mimetype .
+  (if null ext then id else setFilename (page ++ "." ++ ext)) .
+  toResponse . fn
+
+respondLaTeX :: String -> Pandoc -> Handler
+respondLaTeX = respond "application/x-latex" "tex" $
+  writeLaTeX (defaultRespOptions {writerHeader = defaultLaTeXHeader})
+
+respondConTeXt :: String -> Pandoc -> Handler
+respondConTeXt = respond "application/x-context" "tex" $
+  writeConTeXt (defaultRespOptions {writerHeader = defaultConTeXtHeader})
+
+respondRTF :: String -> Pandoc -> Handler
+respondRTF = respond "application/rtf" "rtf" $
+  writeRTF (defaultRespOptions {writerHeader = defaultRTFHeader})
+
+respondRST :: String -> Pandoc -> Handler
+respondRST = respond "text/plain; charset=utf-8" "" $
+  writeRST (defaultRespOptions {writerHeader = "", writerReferenceLinks = True})
+
+respondMan :: String -> Pandoc -> Handler
+respondMan = respond "text/plain; charset=utf-8" "" $
+  writeMan (defaultRespOptions {writerHeader = ""})
+
+respondS5 :: String -> Pandoc -> Handler
+respondS5 _ = ok . toResponse .
+  writeS5 (defaultRespOptions {writerHeader = defaultS5Header,
+            writerS5 = True, writerIncremental = True})
+
+respondTexinfo :: String -> Pandoc -> Handler
+respondTexinfo = respond "application/x-texinfo" "texi" $
+  writeTexinfo (defaultRespOptions {writerHeader = ""})
+
+respondDocbook :: String -> Pandoc -> Handler
+respondDocbook = respond "application/docbook+xml" "xml" $
+  writeDocbook (defaultRespOptions {writerHeader = defaultDocbookHeader})
+
+respondMediaWiki :: String -> Pandoc -> Handler
+respondMediaWiki = respond "text/plain; charset=utf-8" "" $
+  writeMediaWiki (defaultRespOptions {writerHeader = ""})
+
+respondODT :: String -> Pandoc -> Handler
+respondODT page doc = do
+  let openDoc = writeOpenDocument
+                (defaultRespOptions {writerHeader = defaultOpenDocumentHeader})
+                doc
+  conf <- getConfig
+  contents <- liftIO $ withTempDir "gitit-temp-odt" $ \tempdir -> do
+                let tempfile = tempdir </> page <.> "odt"
+                saveOpenDocumentAsODT tempfile (repositoryPath conf) openDoc
+                B.readFile tempfile
+  ok $ setContentType "application/vnd.oasis.opendocument.text" $
+       setFilename (page ++ ".odt") $ (toResponse noHtml) {rsBody = contents}
+
+exportFormats :: [(String, String -> Pandoc -> Handler)]
+exportFormats = [ ("LaTeX",     respondLaTeX)     -- (description, writer)
+                , ("ConTeXt",   respondConTeXt)
+                , ("Texinfo",   respondTexinfo)
+                , ("reST",      respondRST)
+                , ("MediaWiki", respondMediaWiki)
+                , ("man",       respondMan)
+                , ("DocBook",   respondDocbook)
+                , ("S5",        respondS5)
+                , ("ODT",       respondODT)
+                , ("RTF",       respondRTF) ]
diff --git a/Network/Gitit/Feed.hs b/Network/Gitit/Feed.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Feed.hs
@@ -0,0 +1,140 @@
+{-
+Copyright (C) 2009 Gwern Branwen <gwern0@gmail.com> and
+John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- Functions for creating atom feeds for gitit wikis and pages.
+-}
+
+module Network.Gitit.Feed (FeedConfig(..), filestoreToXmlFeed) where
+
+import Text.Atom.Feed
+import Text.Atom.Feed.Export
+import Text.XML.Light
+import Data.FileStore.Types
+import Data.Maybe
+import Data.DateTime
+import System.FilePath
+import Control.Monad
+import Data.List (intercalate, sortBy)
+import Data.Ord (comparing)
+
+data FeedConfig = FeedConfig {
+    fcTitle    :: String
+  , fcBaseUrl  :: String
+  , fcFeedDays :: Integer
+  } deriving (Show, Read)
+
+filestoreToXmlFeed :: FeedConfig -> FileStore -> (Maybe FilePath) -> IO String
+filestoreToXmlFeed cfg f mbPath = filestoreToFeed cfg f mbPath >>= return . ppTopElement . xmlFeed
+
+filestoreToFeed :: FeedConfig -> FileStore -> (Maybe FilePath) -> IO Feed
+filestoreToFeed cfg a mbPath = do
+  let path' = maybe "" id mbPath
+  when (null $ fcBaseUrl cfg) $ error "base-url in the config file is null."
+  rs <- changeLog cfg a mbPath
+  let rsShifted = if null rs
+                     then []
+                     else head rs : init rs   -- so we can get revids for diffs
+  now <- liftM formatFeedTime getCurrentTime
+  return $ Feed { feedId = fcBaseUrl cfg ++ "/" ++ path'
+                , feedTitle = TextString $ fcTitle cfg
+                , feedUpdated = now
+                , feedAuthors = []
+                , feedCategories = []
+                , feedContributors = []
+                , feedGenerator = Just Generator{ genURI = Just "http://github.com/jgm/gitit"
+                                                , genVersion = Nothing
+                                                , genText = "gitit" }
+                , feedIcon = Nothing
+                , feedLinks = []
+                , feedLogo = Nothing
+                , feedRights = Nothing
+                , feedSubtitle = Nothing
+                , feedAttrs = []
+                , feedOther = []  
+                , feedEntries = reverse $ zipWith (revToEntry cfg path') rs rsShifted }
+
+-- | Get the last N days history.
+changeLog :: FeedConfig -> FileStore -> (Maybe FilePath) -> IO [Revision]
+changeLog cfg a mbPath = do
+  let files = maybe [] (\f -> [f, f <.> "page"]) mbPath
+  now <- getCurrentTime
+  let startTime = addMinutes (-60 * 24 * fcFeedDays cfg) now
+  rs <- history a files TimeRange{timeFrom = Just startTime, timeTo = Just now}
+  return $ sortBy (comparing revDateTime) rs
+ 
+revToEntry :: FeedConfig -> String -> Revision -> Revision -> Entry
+revToEntry cfg path' Revision{
+                     revId = rid,
+                     revDateTime = rdt,
+                     revAuthor = ra,
+                     revDescription = rd,
+                     revChanges = rv } prevRevision =
+  baseEntry{ entrySummary = Just $ TextString rd
+           , entryAuthors = [Person { personName = authorName ra
+                                    , personURI = Nothing 
+                                    , personEmail = Just $ authorEmail ra
+                                    , personOther = [] }]
+           , entryLinks = [diffLink]
+
+           -- Comments omitted; needs to be done by Gitit
+           -- only Gitit knows the Url of the Talk: page. See
+           -- http://www.rssboard.org/rss-2-0-1-rv-6#ltcommentsgtSubelementOfLtitemgt
+
+           -- FIXME: True field seems to tell Guid that it's a 'long-term'/'permanent'
+           -- GUID. This may not be correct. See
+           -- https://secure.wikimedia.org/wikipedia/en/wiki/Globally_Unique_Identifier
+           -- entryId = rid,
+
+           -- Source is not entirely relevant, and is only handleable by web software,
+           -- not by a filestore-level function. See
+           -- http://www.rssboard.org/rss-2-0-1-rv-6#ltsourcegtSubelementOfLtitemgt
+
+           -- The following are omitted:
+           -- Category is omitted, see
+           -- http://www.rssboard.org/rss-2-0-1-rv-6#syndic8
+           -- Enclosure seems to be for conveying media, see
+           -- https://secure.wikimedia.org/wikipedia/en/wiki/RSS_enclosure
+        }
+    where diffLink = Link{ linkHref = fcBaseUrl cfg ++ "/_diff/" ++ firstpath ++ "?to=" ++ rid ++ fromrev
+                         , linkRel = Nothing
+                         , linkType = Nothing
+                         , linkHrefLang = Nothing
+                         , linkTitle = Nothing
+                         , linkLength = Nothing
+                         , linkAttrs = []
+                         , linkOther = [] } 
+          (firstpath, fromrev) =
+                      if null path'
+                         then case head rv of
+                                   Modified f -> (dePage f, "&from=" ++ revId prevRevision)
+                                   Added f    -> (dePage f, "")
+                                   Deleted f  -> (dePage f, "&from=" ++ revId prevRevision)
+                         else (path',"")
+          baseEntry = nullEntry (fcBaseUrl cfg ++ "/" ++ path' ++ "?revision=" ++ rid)
+                        (TextString (intercalate ", " $ map showRev rv)) (formatFeedTime rdt)
+          showRev (Modified f) = dePage f
+          showRev (Added f)    = "added " ++ dePage f
+          showRev (Deleted f)  = "deleted " ++ dePage f
+          dePage f = if takeExtension f == ".page"
+                        then dropExtension f
+                        else f
+
+formatFeedTime :: DateTime -> String
+formatFeedTime = formatDateTime "%Y-%m%--%dT%TZ"  -- Why the double hyphen between %m and %d? It works.
+                                                  -- A single hyphen seems to disappear - I don't know why!
diff --git a/Network/Gitit/Framework.hs b/Network/Gitit/Framework.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Framework.hs
@@ -0,0 +1,353 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- | Useful functions for defining wiki handlers.
+-}
+
+module Network.Gitit.Framework (
+                               -- * Combinators for dealing with users 
+                                 withUserFromSession
+                               , withUserFromHTTPAuth
+                               , requireUserThat
+                               , requireUser
+                               , getLoggedInUser
+                               , sessionTime
+                               -- * Combinators to exclude certain actions
+                               , unlessNoEdit
+                               , unlessNoDelete
+                               -- * Guards for routing
+                               , guardCommand
+                               , guardPath
+                               , guardIndex
+                               , guardBareBase
+                               -- * Functions to get info from the request
+                               , getPath
+                               , getPage
+                               , getReferer
+                               , getWikiBase
+                               , uriPath
+                               -- * Useful predicates
+                               , isPage
+                               , isPageFile
+                               , isDiscussPage
+                               , isDiscussPageFile
+                               , isSourceCode
+                               -- * Combinators that change the request locally
+                               , withMessages
+                               , withInput
+                               -- * Miscellaneous
+                               , urlForPage
+                               , pathForPage
+                               , getMimeTypeForExtension
+                               , validate
+                               , filestoreFromConfig
+                               )
+where
+import Network.Gitit.Server
+import Network.Gitit.State
+import Network.Gitit.Types
+import Data.FileStore
+import Data.Char (toLower, isAscii)
+import Control.Monad (mzero, liftM, MonadPlus)
+import qualified Data.Map as M
+import Data.ByteString.UTF8 (toString)
+import Data.ByteString.Lazy.UTF8 (fromString)
+import Data.Maybe (fromJust)
+import Data.List (intercalate, isPrefixOf, isInfixOf, (\\))
+import System.FilePath ((<.>), takeExtension)
+import Text.Highlighting.Kate
+import Text.ParserCombinators.Parsec
+import Network.URL (decString, encString)
+import Happstack.Crypto.Base64 (decode)
+import Network.HTTP (urlEncodeVars)
+
+-- | Run the handler if a user is logged in, otherwise redirect
+-- to login page.
+requireUser :: Handler -> Handler 
+requireUser = requireUserThat (const True)
+
+-- | Run the handler if a user satisfying the predicate is logged in.
+-- Redirect to login if nobody logged in; raise error if someone is
+-- logged in but doesn't satisfy the predicate.
+requireUserThat :: (User -> Bool) -> Handler -> Handler
+requireUserThat predicate handler = do
+  mbUser <- getLoggedInUser
+  rq <- askRq
+  let url = rqUri rq ++ rqQuery rq
+  case mbUser of
+       Nothing   -> tempRedirect ("/_login?" ++ urlEncodeVars [("destination", url)]) $ toResponse ()
+       Just u    -> if predicate u
+                       then handler
+                       else error "Not authorized."
+
+-- | Run the handler after setting @REMOTE_USER@ with the user from
+-- the session.
+withUserFromSession :: Handler -> Handler
+withUserFromSession handler = withData $ \(sk :: Maybe SessionKey) -> do
+  mbSd <- maybe (return Nothing) getSession sk
+  mbUser <- case mbSd of
+            Nothing    -> return Nothing
+            Just sd    -> do
+              addCookie sessionTime (mkCookie "sid" (show $ fromJust sk))  -- refresh timeout
+              getUser $! sessionUser sd
+  let user = maybe "" uUsername mbUser
+  localRq (setHeader "REMOTE_USER" user) handler
+
+-- | Run the handler after setting @REMOTE_USER@ from the "authorization"
+-- header.  Works with simple HTTP authentication or digest authentication.
+withUserFromHTTPAuth :: Handler -> Handler
+withUserFromHTTPAuth handler = do
+  req <- askRq
+  let user = case (getHeader "authorization" req) of
+              Nothing         -> ""
+              Just authHeader -> case parse pAuthorizationHeader "" (toString authHeader) of
+                                  Left _  -> ""
+                                  Right u -> u
+  localRq (setHeader "REMOTE_USER" user) handler
+
+-- | Returns @Just@ logged in user or @Nothing@.
+getLoggedInUser :: GititServerPart (Maybe User)
+getLoggedInUser = do
+  req <- askRq
+  case maybe "" toString (getHeader "REMOTE_USER" req) of
+        "" -> return Nothing
+        u  -> do
+          mbUser <- getUser u
+          case mbUser of
+               Just user -> return $ Just user
+               Nothing   -> return $ Just User{uUsername = u, uEmail = "", uPassword = undefined}
+
+pAuthorizationHeader :: GenParser Char st String
+pAuthorizationHeader = try pBasicHeader <|> pDigestHeader
+
+pDigestHeader :: GenParser Char st String
+pDigestHeader = do
+  string "Digest username=\""
+  result' <- many (noneOf "\"")
+  char '"'
+  return result'
+
+pBasicHeader :: GenParser Char st String
+pBasicHeader = do
+  string "Basic "
+  result' <- many (noneOf " \t\n")
+  return $ takeWhile (/=':') $ decode result'
+
+sessionTime :: Int
+sessionTime = 60 * 60     -- session will expire 1 hour after page request
+
+-- | @unlessNoEdit responder fallback@ runs @responder@ unless the
+-- page has been designated not editable in configuration; in that
+-- case, runs @fallback@.
+unlessNoEdit :: Handler
+             -> Handler
+             -> Handler
+unlessNoEdit responder fallback = withData $ \(params :: Params) -> do
+  cfg <- getConfig
+  page <- getPage
+  if page `elem` noEdit cfg
+     then withMessages ("Page is locked." : pMessages params) fallback
+     else responder
+
+-- | @unlessNoDelete responder fallback@ runs @responder@ unless the
+-- page has been designated not deletable in configuration; in that
+-- case, runs @fallback@.
+unlessNoDelete :: Handler
+               -> Handler
+               -> Handler
+unlessNoDelete responder fallback = withData $ \(params :: Params) -> do
+  cfg <- getConfig
+  page <- getPage
+  if page `elem` noDelete cfg
+     then withMessages ("Page cannot be deleted." : pMessages params) fallback
+     else responder
+
+-- | Returns the current path (subtracting initial commands like @\/_edit@).
+getPath :: ServerMonad m => m String
+getPath = liftM (fromJust . decString True . intercalate "/" . rqPaths) askRq
+
+-- | Returns the current page name (derived from the path).
+getPage :: GititServerPart String
+getPage = do
+  conf <- getConfig
+  path' <- getPath
+  if null path'
+     then return (frontPage conf)
+     else if isPage path'
+             then return path'
+             else mzero  -- fail if not valid page name
+
+-- | Returns the contents of the "referer" header.
+getReferer :: ServerMonad m => m String
+getReferer = do
+  req <- askRq
+  base' <- getWikiBase
+  return $ case getHeader "referer" req of
+                 Just r  -> case toString r of
+                                 ""  -> base'
+                                 s   -> s
+                 Nothing -> base'
+
+-- | Returns the base URL of the wiki in the happstack server.
+-- So, if the wiki handlers are behind a @dir 'foo'@, getWikiBase will
+-- return @\/foo/@.  getWikiBase doesn't know anything about HTTP
+-- proxies, so if you use proxies to map a gitit wiki to @\/foo/@,
+-- you'll still need to follow the instructions in README.
+getWikiBase :: ServerMonad m => m String
+getWikiBase = do
+  path' <- getPath
+  uri' <- liftM (fromJust . decString True . rqUri) askRq
+  case calculateWikiBase path' uri' of
+       Just b    -> return b
+       Nothing   -> error $ "Could not getWikiBase: (path, uri) = " ++ show (path',uri')
+
+-- | The pure core of 'getWikiBase'.
+calculateWikiBase :: String -> String -> Maybe String
+calculateWikiBase path' uri' =
+  let revpaths = reverse . filter (not . null) $ splitOn '/' path'
+      revuris  = reverse . filter (not . null) $ splitOn '/' uri'
+  in  if revpaths `isPrefixOf` revuris
+         then let revbase = drop (length revpaths) revuris
+                  -- a path like _feed is not part of the base...
+                  revbase' = case revbase of
+                             (x:xs) | startsWithUnderscore x -> xs
+                             xs                              -> xs
+                  base'    = intercalate "/" $ reverse revbase'
+              in  Just $ if null base' then "" else '/' : base'
+          else Nothing
+
+startsWithUnderscore :: String -> Bool
+startsWithUnderscore ('_':_) = True
+startsWithUnderscore _ = False
+
+splitOn :: Eq a => a -> [a] -> [[a]]
+splitOn c cs =
+  let (next, rest) = break (==c) cs
+  in  if null rest
+         then [next]
+         else next : splitOn c (tail rest)
+
+-- | Returns path portion of URI, without initial @\/@.
+-- Consecutive spaces are collapsed.  We don't want to distinguish
+-- @Hi There@ and @Hi  There@.
+uriPath :: String -> String
+uriPath = unwords . words . drop 1 . takeWhile (/='?')
+
+isPage :: String -> Bool
+isPage "" = False
+isPage ('_':_) = False
+isPage s = all (`notElem` "*?") s && not (".." `isInfixOf` s) && not ("/_" `isInfixOf` s)
+-- for now, we disallow @*@ and @?@ in page names, because git filestore
+-- does not deal with them properly, and darcs filestore disallows them.
+
+isPageFile :: FilePath -> Bool
+isPageFile f = takeExtension f == ".page"
+
+isDiscussPage :: String -> Bool
+isDiscussPage ('@':xs) = isPage xs
+isDiscussPage _ = False
+
+isDiscussPageFile :: FilePath -> Bool
+isDiscussPageFile ('@':xs) = isPageFile xs
+isDiscussPageFile _ = False
+
+isSourceCode :: String -> Bool
+isSourceCode path' =
+  let ext   = map toLower $ takeExtension path'
+      langs = languagesByExtension ext \\ ["Postscript"]
+  in  not . null $ langs
+
+-- | Returns encoded URL path for the page with the given name, relative to
+-- the wiki base.
+urlForPage :: String -> String
+urlForPage page = "/" ++
+  encString True (\c -> isAscii c && (c `notElem` "?&")) page
+-- / and @ are left unescaped so that browsers recognize relative URLs and talk pages correctly
+
+-- | Returns the filestore path of the file containing the page's source.
+pathForPage :: String -> FilePath
+pathForPage page = page <.> "page"
+
+-- | Retrieves a mime type based on file extension.
+getMimeTypeForExtension :: String -> GititServerPart String
+getMimeTypeForExtension ext = do
+  mimes <- liftM mimeMap getConfig
+  return $ case M.lookup (dropWhile (=='.') $ map toLower ext) mimes of
+                Nothing -> "application/octet-stream"
+                Just t  -> t
+
+-- | Simple helper for validation of forms.
+validate :: [(Bool, String)]   -- ^ list of conditions and error messages
+         -> [String]           -- ^ list of error messages
+validate = foldl go []
+   where go errs (condition, msg) = if condition then msg:errs else errs
+
+guardCommand :: String -> GititServerPart ()
+guardCommand command = withData $ \(com :: Command) ->
+  case com of
+       Command (Just c) | c == command -> return ()
+       _                               -> mzero
+
+guardPath :: (String -> Bool) -> GititServerPart ()
+guardPath pred' = guardRq (pred' . rqUri)
+
+-- | Succeeds if path is an index path:  e.g. @\/foo\/bar/@.
+guardIndex :: GititServerPart ()
+guardIndex = do
+  base <- getWikiBase
+  uri' <- liftM rqUri askRq
+  let localpath = drop (length base) uri'
+  if length localpath > 1 && last uri' == '/'
+     then return ()
+     else mzero
+
+-- Guard against a path like @\/wiki@ when the wiki is being
+-- served at @\/wiki@.
+guardBareBase :: GititServerPart ()
+guardBareBase = do
+  base' <- getWikiBase
+  uri' <- liftM rqUri askRq
+  if not (null base') && base' == uri'
+     then return ()
+     else mzero
+
+-- | Runs a server monad in a local context after setting
+-- the "messages" request header.
+withMessages :: ServerMonad m => [String] -> m a -> m a
+withMessages = withInput "messages" . show
+
+-- | Runs a server monad in a local context after setting
+-- request header.
+withInput :: ServerMonad m => String -> String -> m a -> m a
+withInput name val handler = do
+  req <- askRq
+  let inps = filter (\(n,_) -> n /= name) $ rqInputs req
+  let newInp = (name, Input { inputValue = fromString val
+                            , inputFilename = Nothing
+                            , inputContentType = ContentType {
+                                    ctType = "text"
+                                  , ctSubtype = "plain"
+                                  , ctParameters = [] }
+                            })
+  localRq (\rq -> rq{ rqInputs = newInp : inps }) handler
+
+-- | Returns a filestore object derived from the
+-- repository path and filestore type specified in configuration.
+filestoreFromConfig :: Config -> FileStore
+filestoreFromConfig conf =
+  case repositoryType conf of
+         Git   -> gitFileStore $ repositoryPath conf
+         Darcs -> darcsFileStore $ repositoryPath conf
diff --git a/Network/Gitit/Handlers.hs b/Network/Gitit/Handlers.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Handlers.hs
@@ -0,0 +1,768 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-
+Copyright (C) 2008-9 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- Handlers for wiki functions.
+-}
+
+module Network.Gitit.Handlers (
+                        handleAny
+                      , debugHandler
+                      , randomPage
+                      , discussPage
+                      , createPage
+                      , showActivity
+                      , goToPage
+                      , searchResults
+                      , uploadForm
+                      , uploadFile
+                      , indexPage
+                      , categoryPage
+                      , categoryListPage
+                      , preview
+                      , showRawPage
+                      , showFileAsText
+                      , showPageHistory
+                      , showFileHistory
+                      , showPage
+                      , showPageDiff
+                      , showFileDiff
+                      , exportPage
+                      , updatePage
+                      , editPage
+                      , deletePage
+                      , confirmDelete
+                      , showHighlightedSource
+                      , currentUser
+                      , expireCache
+                      , feedHandler
+                      )
+where
+import Data.FileStore
+import Network.Gitit.Server
+import Network.Gitit.Framework
+import Network.Gitit.Layout
+import Network.Gitit.State
+import Network.Gitit.Types
+import Network.Gitit.Feed (filestoreToXmlFeed, FeedConfig(..))
+import Network.Gitit.Util (orIfNull)
+import Network.Gitit.Cache (expireCachedFile, lookupCache, cacheContents)
+import Network.Gitit.ContentTransformer (showRawPage, showFileAsText, showPage,
+        exportPage, showHighlightedSource, preview, applyPreCommitPlugins)
+import Network.Gitit.Page (extractCategories)
+import Control.Exception (throwIO, catch, try)
+import Prelude hiding (writeFile, readFile, catch)
+import Data.ByteString.UTF8 (toString)
+import System.Time
+import System.FilePath
+import System.IO.UTF8 (readFile)
+import Network.Gitit.State
+import Text.XHtml hiding ( (</>), dir, method, password, rev )
+import qualified Text.XHtml as X ( method )
+import Data.List (intersperse, nub, sortBy, find, isPrefixOf, inits, sort)
+import Data.Maybe (fromMaybe, mapMaybe, isJust, catMaybes)
+import Data.Ord (comparing)
+import Data.Char (toLower)
+import Control.Monad.Reader
+import qualified Data.ByteString.Lazy as B
+import qualified Data.ByteString as S
+import Network.HTTP (urlEncodeVars)
+import Data.DateTime (getCurrentTime, addMinutes)
+import Data.FileStore
+import System.Log.Logger (logM, Priority(..))
+
+handleAny :: Handler
+handleAny = uriRest $ \uri ->
+  let path' = uriPath uri
+  in  do fs <- getFileStore
+         mimetype <- getMimeTypeForExtension
+                      (takeExtension path')
+         res <- liftIO $ try
+                (retrieve fs path' Nothing :: IO B.ByteString)
+         case res of
+                Right contents -> ignoreFilters >>  -- don't compress
+                                  (ok $ setContentType mimetype $
+                                    (toResponse noHtml) {rsBody = contents})
+                                    -- ugly hack
+                Left NotFound  -> anyRequest mzero
+                Left e         -> error (show e)
+
+debugHandler :: Handler
+debugHandler = withData $ \(params :: Params) -> do
+  req <- askRq
+  liftIO $ logM "gitit" DEBUG (show req)
+  page <- getPage
+  liftIO $ logM "gitit" DEBUG $ "Page = '" ++ page ++ "'\n" ++
+              show params
+  mzero
+
+randomPage :: Handler
+randomPage = do
+  fs <- getFileStore
+  files <- liftIO $ index fs
+  let pages = map dropExtension $
+                filter (\f -> isPageFile f && not (isDiscussPageFile f)) files
+  base' <- getWikiBase
+  if null pages
+     then error "No pages found!"
+     else do
+       TOD _ picosecs <- liftIO getClockTime
+       let newPage = pages !!
+                     ((fromIntegral picosecs `div` 1000000) `mod` length pages)
+       seeOther (base' ++ urlForPage newPage) $ toResponse $
+         p << "Redirecting to a random page"
+
+discussPage :: Handler
+discussPage = do
+  page <- getPage
+  base' <- getWikiBase
+  seeOther (base' ++ urlForPage (if isDiscussPage page then page else ('@':page))) $
+                     toResponse "Redirecting to discussion page"
+
+createPage :: Handler
+createPage = do
+  page <- getPage
+  base' <- getWikiBase
+  case page of
+       ('_':_) -> mzero   -- don't allow creation of _index, etc.
+       _       -> formattedPage defaultPageLayout{
+                                      pgPageName = page
+                                    , pgTabs = []
+                                    , pgTitle = "Create " ++ page ++ "?"
+                                    } $
+                    p << [ stringToHtml ("There is no page '" ++ page ++
+                              "'.  You may create the page by "),
+                            anchor ! [href $ base' ++ "/_edit" ++ urlForPage page] <<
+                              "clicking here." ]
+
+uploadForm :: Handler
+uploadForm = withData $ \(params :: Params) -> do
+  let origPath = pFilename params
+  let wikiname = pWikiname params `orIfNull` takeFileName origPath
+  let logMsg = pLogMsg params
+  let upForm = form ! [X.method "post", enctype "multipart/form-data"] <<
+       fieldset <<
+       [ p << [label << "File to upload:"
+              , br
+              , afile "file" ! [value origPath] ]
+       , p << [ label << "Name on wiki, including extension"
+              , noscript << " (leave blank to use the same filename)"
+              , stringToHtml ":"
+              , br
+              , textfield "wikiname" ! [value wikiname]
+              , primHtmlChar "nbsp"
+              , checkbox "overwrite" "yes"
+              , label << "Overwrite existing file" ]
+       , p << [ label << "Description of content or changes:"
+              , br
+              , textfield "logMsg" ! [size "60", value logMsg]
+              , submit "upload" "Upload" ]
+       ]
+  formattedPage defaultPageLayout{
+                   pgMessages = pMessages params,
+                   pgScripts = ["uploadForm.js"],
+                   pgShowPageTools = False,
+                   pgTabs = [],
+                   pgTitle = "Upload a file"} upForm
+
+uploadFile :: Handler
+uploadFile = withData $ \(params :: Params) -> do
+  let origPath = pFilename params
+  let fileContents = pFileContents params
+  let wikiname = pWikiname params `orIfNull` takeFileName origPath
+  let logMsg = pLogMsg params
+  cfg <- getConfig
+  mbUser <- getLoggedInUser
+  (user, email) <- case mbUser of
+                        Nothing -> fail "User must be logged in to delete page."
+                        Just u  -> return (uUsername u, uEmail u)
+  let overwrite = pOverwrite params
+  fs <- getFileStore
+  exists <- liftIO $ catch (latest fs wikiname >> return True) $ \e ->
+                      if e == NotFound
+                         then return False
+                         else throwIO e >> return True
+  let imageExtensions = [".png", ".jpg", ".gif"]
+  let errors = validate
+                 [ (null logMsg, "Description cannot be empty.")
+                 , (null origPath, "File not found.")
+                 , (not overwrite && exists, "A file named '" ++ wikiname ++
+                    "' already exists in the repository: choose a new name " ++
+                    "or check the box to overwrite the existing file.")
+                 , (B.length fileContents > fromIntegral (maxUploadSize cfg),
+                    "File exceeds maximum upload size.")
+                 , (isPageFile wikiname,
+                    "This file extension is reserved for wiki pages.")
+                 ]
+  if null errors
+     then do
+       expireCachedFile wikiname `mplus` return ()
+       liftIO $ save fs wikiname (Author user email) logMsg fileContents
+       let contents = thediv <<
+             [ h2 << ("Uploaded " ++ show (B.length fileContents) ++ " bytes")
+             , if takeExtension wikiname `elem` imageExtensions
+                  then p << "To add this image to a page, use:" +++
+                       pre << ("![alt text](/" ++ wikiname ++ ")")
+                  else p << "To link to this resource from a page, use:" +++
+                       pre << ("[link label](/" ++ wikiname ++ ")") ]
+       formattedPage defaultPageLayout{
+                       pgMessages = pMessages params,
+                       pgShowPageTools = False,
+                       pgTabs = [],
+                       pgTitle = "Upload successful"}
+                     contents
+     else withMessages errors uploadForm
+
+goToPage :: Handler
+goToPage = withData $ \(params :: Params) -> do
+  let gotopage = pGotoPage params
+  fs <- getFileStore
+  allPageNames <- liftM (map dropExtension . filter isPageFile) $
+                   liftIO $ index fs
+  let findPage f = find f allPageNames
+  let exactMatch f = gotopage == f
+  let insensitiveMatch f = (map toLower gotopage) == (map toLower f)
+  let prefixMatch f = (map toLower gotopage) `isPrefixOf` (map toLower f)
+  base' <- getWikiBase
+  case findPage exactMatch of
+       Just m  -> seeOther (base' ++ urlForPage m) $ toResponse
+                     "Redirecting to exact match"
+       Nothing -> case findPage insensitiveMatch of
+                       Just m  -> seeOther (base' ++ urlForPage m) $ toResponse
+                                    "Redirecting to case-insensitive match"
+                       Nothing -> case findPage prefixMatch of
+                                       Just m  -> seeOther (base' ++ urlForPage m) $
+                                                  toResponse $ "Redirecting" ++
+                                                    " to partial match"
+                                       Nothing -> withInput "patterns" gotopage searchResults
+
+searchResults :: Handler
+searchResults = withData $ \(params :: Params) -> do
+  let patterns = pPatterns params
+  fs <- getFileStore
+  matchLines <- if null patterns
+                   then return []
+                   else liftIO $ search fs SearchQuery{
+                                            queryPatterns = patterns
+                                          , queryWholeWords = True
+                                          , queryMatchAll = True
+                                          , queryIgnoreCase = True }
+  let contentMatches = map matchResourceName matchLines
+  allPages <- liftM (filter isPageFile) $ liftIO $ index fs
+  let inPageName pageName' x = x `elem` (words $ map toLower $
+                                          dropExtension pageName')
+  let matchesPatterns pageName' = all (inPageName pageName') $
+                                    map (map toLower) patterns
+  let pageNameMatches = filter matchesPatterns allPages
+  let allMatchedFiles = nub $ filter isPageFile contentMatches ++
+                              pageNameMatches
+  let matchesInFile f =  mapMaybe (\x -> if matchResourceName x == f
+                                            then Just (matchLine x)
+                                            else Nothing) matchLines
+  let matches = map (\f -> (f, matchesInFile f)) allMatchedFiles
+  let relevance (f, ms) = length ms + if f `elem` pageNameMatches
+                                         then 100
+                                         else 0
+  let preamble = if null matches
+                    then h3 << if null patterns
+                                  then ["Please enter a search term."]
+                                  else ["No matches found for '",
+                                         unwords patterns, "':"]
+                    else h3 << [(show $ length matches),
+                                " matches found for '", unwords patterns, "':"]
+  base' <- getWikiBase
+  let toMatchListItem (file, contents) = li <<
+        [ anchor ! [href $ base' ++ urlForPage (dropExtension file)] << dropExtension file
+        , stringToHtml (" (" ++ show (length contents) ++ " matching lines)")
+        , stringToHtml " "
+        , anchor ! [href "#", theclass "showmatch",
+                    thestyle "display: none;"] << if length contents > 0
+                                                     then "[show matches]"
+                                                     else ""
+        , pre ! [theclass "matches"] << unlines contents]
+  let htmlMatches = preamble +++
+                    olist << map toMatchListItem
+                             (reverse $ sortBy (comparing relevance) matches)
+  formattedPage defaultPageLayout{
+                  pgMessages = pMessages params,
+                  pgShowPageTools = False,
+                  pgTabs = [],
+                  pgScripts = ["search.js"],
+                  pgTitle = "Search results"}
+                htmlMatches
+
+showPageHistory :: Handler
+showPageHistory = withData $ \(params :: Params) -> do
+  page <- getPage
+  showHistory (pathForPage page) page params
+
+showFileHistory :: Handler
+showFileHistory = withData $ \(params :: Params) -> do
+  file <- getPage
+  showHistory file file params
+
+showHistory :: String -> String -> Params -> Handler
+showHistory file page params =  do
+  currTime <- liftIO getCurrentTime
+  let oneYearAgo = addMinutes (-1 * 60 * 24 * 365) currTime
+  let since = case pSince params of
+                   Nothing -> Just oneYearAgo
+                   Just t  -> Just t
+  fs <- getFileStore
+  hist <- liftIO $ history fs [file] (TimeRange since Nothing)
+  base' <- getWikiBase
+  let versionToHtml rev pos = li ! [theclass "difflink", intAttr "order" pos,
+                                    strAttr "revision" (revId rev),
+                                    strAttr "diffurl" (base' ++ "/_diff/" ++ page)] <<
+        [ thespan ! [theclass "date"] << (show $ revDateTime rev)
+        , stringToHtml " ("
+        , thespan ! [theclass "author"] << anchor ! [href $ base' ++ "/_activity?" ++
+            urlEncodeVars [("forUser", authorName $ revAuthor rev)]] <<
+              (authorName $ revAuthor rev)
+        , stringToHtml "): "
+        , anchor ! [href (base' ++ urlForPage page ++ "?revision=" ++ revId rev)] <<
+           thespan ! [theclass "subject"] <<  revDescription rev
+        , noscript <<
+            ([ stringToHtml " [compare with "
+             , anchor ! [href $ base' ++ "/_diff" ++ urlForPage page ++ "?to=" ++ revId rev] <<
+                  "previous" ] ++
+             (if pos /= 1
+                  then [ primHtmlChar "nbsp"
+                       , primHtmlChar "bull"
+                       , primHtmlChar "nbsp"
+                       , anchor ! [href $ base' ++ "/_diff" ++ urlForPage page ++ "?from=" ++
+                                  revId rev] << "current"
+                       ]
+                  else []) ++
+             [stringToHtml "]"])
+        ]
+  if null hist
+     then mzero
+     else do
+       let contents = ulist ! [theclass "history"] <<
+                        zipWith versionToHtml hist
+                        [(length hist), (length hist - 1)..1]
+       let tabs = if file == page  -- source file, not wiki page
+                     then [ViewTab,HistoryTab]
+                     else pgTabs defaultPageLayout
+       formattedPage defaultPageLayout{
+                        pgPageName = page,
+                        pgMessages = pMessages params,
+                        pgScripts = ["dragdiff.js"],
+                        pgTabs = tabs,
+                        pgSelectedTab = HistoryTab,
+                        pgTitle = ("Changes to " ++ page)
+                        } contents
+
+showActivity :: Handler
+showActivity = withData $ \(params :: Params) -> do
+  currTime <- liftIO getCurrentTime
+  let oneMonthAgo = addMinutes (-1 * 60 * 24 * 30) currTime
+  let since = case pSince params of
+                   Nothing -> Just oneMonthAgo
+                   Just t  -> Just t
+  let forUser = pForUser params
+  fs <- getFileStore
+  hist <- liftIO $ history fs [] (TimeRange since Nothing)
+  let hist' = case forUser of
+                   Nothing -> hist
+                   Just u  -> filter (\r -> authorName (revAuthor r) == u) hist
+  let fileFromChange (Added f)    = f
+      fileFromChange (Modified f) = f
+      fileFromChange (Deleted f)  = f
+  let dropDotPage file = if isPageFile file
+                            then dropExtension file
+                            else file
+  base' <- getWikiBase
+  let fileAnchor revis file =
+        anchor ! [href $ base' ++ "/_diff" ++ urlForPage file ++ "?to=" ++ revis] << file
+  let filesFor changes revis = intersperse (primHtmlChar "nbsp") $
+        map (fileAnchor revis . dropDotPage . fileFromChange) changes
+  let heading = h1 << ("Recent changes by " ++ fromMaybe "all users" forUser)
+  let revToListItem rev = li <<
+        [ thespan ! [theclass "date"] << (show $ revDateTime rev)
+        , stringToHtml " ("
+        , thespan ! [theclass "author"] <<
+            anchor ! [href $ base' ++ "/_activity?" ++
+              urlEncodeVars [("forUser", authorName $ revAuthor rev)]] <<
+                (authorName $ revAuthor rev)
+        , stringToHtml "): "
+        , thespan ! [theclass "subject"] << revDescription rev
+        , stringToHtml " ("
+        , thespan ! [theclass "files"] << filesFor (revChanges rev) (revId rev)
+        , stringToHtml ")"
+        ]
+  let contents = ulist ! [theclass "history"] << map revToListItem hist'
+  formattedPage defaultPageLayout{
+                  pgMessages = pMessages params,
+                  pgShowPageTools = False,
+                  pgTabs = [],
+                  pgTitle = "Recent changes"
+                  } (heading +++ contents)
+
+showPageDiff :: Handler
+showPageDiff = withData $ \(params :: Params) -> do
+  page <- getPage
+  showDiff (pathForPage page) page params
+
+showFileDiff :: Handler
+showFileDiff = withData $ \(params :: Params) -> do
+  page <- getPage
+  showDiff page page params
+
+showDiff :: String -> String -> Params -> Handler
+showDiff file page params = do
+  let from = pFrom params
+  let to = pTo params
+  -- 'to' or 'from' must be given
+  when (from == Nothing && to == Nothing) mzero
+  fs <- getFileStore
+  -- if 'to' is not specified, defaults to current revision
+  -- if 'from' is not specified, defaults to revision immediately before 'to'
+  from' <- case (from, to) of
+              (Just _, _)        -> return from
+              (Nothing, Nothing) -> return from
+              (Nothing, Just t)  -> do 
+                pageHist <- liftIO $ history fs [file]
+                                     (TimeRange Nothing Nothing)
+                let (_, upto) = break (\r -> idsMatch fs (revId r) t)
+                                  pageHist
+                return $ if length upto >= 2
+                            -- immediately preceding revision
+                            then Just $ revId $ upto !! 1
+                            else Nothing
+  result' <- liftIO $ try $ getDiff fs file from' to
+  case result' of
+       Left NotFound  -> mzero
+       Left e         -> liftIO $ throwIO e
+       Right htmlDiff -> formattedPage defaultPageLayout{
+                                          pgPageName = page,
+                                          pgRevision = from',
+                                          pgMessages = pMessages params,
+                                          pgTabs = DiffTab :
+                                                   pgTabs defaultPageLayout,
+                                          pgSelectedTab = DiffTab
+                                          }
+                                       htmlDiff
+
+getDiff :: FileStore -> FilePath -> Maybe RevisionId -> Maybe RevisionId
+        -> IO Html
+getDiff fs file from to = do
+  rawDiff <- diff fs file from to
+  let diffLineToHtml (B, xs) = thespan << unlines xs
+      diffLineToHtml (F, xs) = thespan ! [theclass "deleted"] << unlines xs
+      diffLineToHtml (S, xs) = thespan ! [theclass "added"]   << unlines xs
+  return $ h2 ! [theclass "revision"] <<
+             ("Changes from " ++ fromMaybe "beginning" from ++
+              " to " ++ fromMaybe "current" to) +++
+           pre ! [theclass "diff"] << map diffLineToHtml rawDiff
+
+editPage :: Handler
+editPage = withData $ \(params :: Params) -> do
+  let rev = pRevision params  -- if this is set, we're doing a revert
+  fs <- getFileStore
+  page <- getPage
+  let getRevisionAndText = catch
+        (do c <- liftIO $ retrieve fs (pathForPage page) rev
+            -- even if pRevision is set, we return revId of latest
+            -- saved version (because we're doing a revert and
+            -- we don't want gitit to merge the changes with the
+            -- latest version)
+            r <- liftIO $ latest fs (pathForPage page) >>= revision fs
+            return (Just $ revId r, c))
+        (\e -> if e == NotFound
+                  then return (Nothing, "")
+                  else throwIO e)
+  (mbRev, raw) <- case pEditedText params of
+                         Nothing -> liftIO getRevisionAndText
+                         Just t  -> let r = if null (pSHA1 params)
+                                               then Nothing
+                                               else Just (pSHA1 params)
+                                    in return (r, t)
+  let messages = pMessages params
+  let logMsg = pLogMsg params
+  let sha1Box = case mbRev of
+                 Just r  -> textfield "sha1" ! [thestyle "display: none",
+                                                value r]
+                 Nothing -> noHtml
+  let readonly = if isJust (pRevision params)
+                    -- disable editing of text box if it's a revert
+                    then [strAttr "readonly" "yes",
+                          strAttr "style" "color: gray"]
+                    else []
+  base' <- getWikiBase
+  cfg <- getConfig
+  let editForm = gui (base' ++ urlForPage page) ! [identifier "editform"] <<
+                   [ sha1Box
+                   , textarea ! (readonly ++ [cols "80", name "editedText",
+                                  identifier "editedText"]) << raw
+                   , br
+                   , label << "Description of changes:"
+                   , br
+                   , textfield "logMsg" ! (readonly ++ [value logMsg])
+                   , submit "update" "Save"
+                   , primHtmlChar "nbsp"
+                   , submit "cancel" "Discard"
+                   , primHtmlChar "nbsp"
+                   , input ! [thetype "button", theclass "editButton",
+                              identifier "previewButton",
+                              strAttr "onClick" "updatePreviewPane();",
+                              strAttr "style" "display: none;",
+                              value "Preview" ]
+                   , thediv ! [ identifier "previewpane" ] << noHtml
+                   ]
+  formattedPage defaultPageLayout{
+                  pgPageName = page,
+                  pgMessages = messages,
+                  pgRevision = rev,
+                  pgShowPageTools = False,
+                  pgShowSiteNav = False,
+                  pgMarkupHelp = Just $ markupHelp cfg,
+                  pgSelectedTab = EditTab,
+                  pgScripts = ["preview.js"],
+                  pgTitle = ("Editing " ++ page)
+                  } editForm
+
+confirmDelete :: Handler
+confirmDelete = do
+  page <- getPage
+  fs <- getFileStore
+  -- determine whether there is a corresponding page, and if not whether there
+  -- is a corresponding file
+  pageTest <- liftIO $ try $ latest fs (pathForPage page)
+  fileToDelete <- case pageTest of
+                       Right _        -> return $ pathForPage page  -- a page
+                       Left  NotFound -> do
+                         fileTest <- liftIO $ try $ latest fs page
+                         case fileTest of
+                              Right _       -> return page  -- a source file
+                              Left NotFound -> return ""
+                              Left e        -> fail (show e)
+                       Left e        -> fail (show e)
+  let confirmForm = gui "" <<
+        [ p << "Are you sure you want to delete this page?"
+        , input ! [thetype "text", name "filetodelete",
+                   strAttr "style" "display: none;", value fileToDelete]
+        , submit "confirm" "Yes, delete it!"
+        , stringToHtml " "
+        , submit "cancel" "No, keep it!"
+        , br ]
+  formattedPage defaultPageLayout{ pgTitle = "Delete " ++ page ++ "?" } $
+    if null fileToDelete
+       then ulist ! [theclass "messages"] << li <<
+            "There is no file or page by that name."
+       else confirmForm
+
+deletePage :: Handler
+deletePage = withData $ \(params :: Params) -> do
+  page <- getPage
+  let file = pFileToDelete params
+  mbUser <- getLoggedInUser
+  (user, email) <- case mbUser of
+                        Nothing -> fail "User must be logged in to delete."
+                        Just u  -> return (uUsername u, uEmail u)
+  let author = Author user email
+  let descrip = "Deleted using web interface."
+  base' <- getWikiBase
+  if pConfirm params && (file == page || file == page <.> "page") 
+     then do
+       fs <- getFileStore
+       liftIO $ delete fs file author descrip
+       seeOther (base' ++ "/") $ toResponse $ p << "File deleted"
+     else seeOther (base' ++ urlForPage page) $ toResponse $ p << "Not deleted"
+
+updatePage :: Handler
+updatePage = withData $ \(params :: Params) -> do
+  page <- getPage
+  cfg <- getConfig
+  mbUser <- getLoggedInUser
+  (user, email) <- case mbUser of
+                        Nothing -> fail "User must be logged in to delete page."
+                        Just u  -> return (uUsername u, uEmail u)
+  editedText <- case pEditedText params of
+                     Nothing -> error "No body text in POST request"
+                     Just b  -> applyPreCommitPlugins b
+  let logMsg = pLogMsg params `orIfNull` defaultSummary cfg
+  let oldSHA1 = pSHA1 params
+  fs <- getFileStore
+  base' <- getWikiBase
+  if null logMsg
+     then withMessages ["Description cannot be empty."] editPage
+     else do
+       when (length editedText > fromIntegral (maxUploadSize cfg)) $
+          error "Page exceeds maximum size."
+       -- check SHA1 in case page has been modified, merge
+       modifyRes <- if null oldSHA1
+                       then liftIO $ create fs (pathForPage page)
+                                       (Author user email) logMsg editedText >>
+                                     return (Right ())
+                       else do
+                         expireCachedFile (pathForPage page) `mplus` return ()
+                         liftIO $ catch (modify fs (pathForPage page)
+                                            oldSHA1 (Author user email) logMsg
+                                            editedText)
+                                     (\e -> if e == Unchanged
+                                               then return (Right ())
+                                               else throwIO e)
+       case modifyRes of
+            Right () -> seeOther (base' ++ urlForPage page) $ toResponse $ p << "Page updated"
+            Left (MergeInfo mergedWithRev conflicts mergedText) -> do
+               let mergeMsg = "The page has been edited since you checked it out. " ++
+                      "Changes from revision " ++ revId mergedWithRev ++
+                      " have been merged into your edits below. " ++
+                      if conflicts
+                         then "Please resolve conflicts and Save."
+                         else "Please review and Save."
+               withMessages [mergeMsg] $
+                 withInput "editedText" mergedText $
+                   withInput "sha1" (revId mergedWithRev) editPage
+
+indexPage :: Handler
+indexPage = do
+  path' <- getPath
+  base' <- getWikiBase
+  let prefix' = if null path' then "" else path' ++ "/"
+  fs <- getFileStore
+  listing <- liftIO $ directory fs prefix'
+  let isDiscussionPage (FSFile f) = isDiscussPageFile f
+      isDiscussionPage (FSDirectory _) = False
+  let prunedListing = filter (not . isDiscussionPage) listing
+  let htmlIndex = fileListToHtml base' prefix' prunedListing
+  formattedPage defaultPageLayout{
+                  pgPageName = prefix',
+                  pgShowPageTools = False,
+                  pgTabs = [],
+                  pgScripts = [],
+                  pgTitle = "Contents"} htmlIndex
+
+fileListToHtml :: String -> String -> [Resource] -> Html
+fileListToHtml base' prefix files =
+  let fileLink (FSFile f) | isPageFile f =
+        li ! [theclass "page"  ] <<
+          anchor ! [href $ base' ++ "/" ++ prefix ++ dropExtension f] << dropExtension f
+      fileLink (FSFile f) =
+        li ! [theclass "upload"] << anchor ! [href $ base' ++ "/" ++ prefix ++ f] << f
+      fileLink (FSDirectory f) =
+        li ! [theclass "folder"] <<
+          anchor ! [href $ base' ++ "/" ++ prefix ++ f ++ "/"] << f
+      updirs = drop 1 $ inits $ splitPath $ '/' : prefix
+      uplink = foldr (\d accum ->
+                  concatHtml [ anchor ! [theclass "updir",
+                                         href $ if length d == 1
+                                                   then base' ++ "/_index"
+                                                   else base' ++ joinPath d] <<
+                  last d, accum]) noHtml updirs
+  in uplink +++ ulist ! [theclass "index"] << map fileLink files
+
+-- NOTE:  The current implementation of categoryPage does not go via the
+-- filestore abstraction.  That is bad, but can only be fixed if we add
+-- more sophisticated searching options to filestore.
+categoryPage :: Handler
+categoryPage = do
+  category <- getPath
+  cfg <- getConfig
+  let repoPath = repositoryPath cfg
+  let categoryDescription = "Category: " ++ category
+  fs <- getFileStore
+  files <- liftIO $ index fs
+  let pages = filter (\f -> isPageFile f && not (isDiscussPageFile f)) files
+  matches <- liftM catMaybes $
+             forM pages $ \f ->
+               liftIO (readFile $ repoPath </> f) >>= \s ->
+               return $ if category `elem` (extractCategories s)
+                           then Just $ dropExtension f
+                           else Nothing
+  base' <- getWikiBase
+  let toMatchListItem file = li <<
+        [ anchor ! [href $ base' ++ urlForPage (dropExtension file)] << dropExtension file ]
+  let htmlMatches = ulist << map toMatchListItem matches
+  formattedPage defaultPageLayout{
+                  pgPageName = categoryDescription,
+                  pgShowPageTools = False,
+                  pgTabs = [],
+                  pgScripts = ["search.js"],
+                  pgTitle = categoryDescription }
+                htmlMatches
+
+categoryListPage :: Handler
+categoryListPage = do
+  cfg <- getConfig
+  let repoPath = repositoryPath cfg
+  fs <- getFileStore
+  files <- liftIO $ index fs
+  let pages = filter (\f -> isPageFile f && not (isDiscussPageFile f)) files
+  categories <- liftIO $
+                liftM (nub . sort . concat) $
+                forM pages $
+                liftM extractCategories . (readFile . (repoPath </>))
+  base' <- getWikiBase
+  let toCatLink ctg = li <<
+        [ anchor ! [href $ base' ++ "/_category/" ++ ctg] << ctg ]
+  let htmlMatches = ulist << map toCatLink categories
+  formattedPage defaultPageLayout{
+                  pgPageName = "Categories",
+                  pgShowPageTools = False,
+                  pgTabs = [],
+                  pgScripts = ["search.js"],
+                  pgTitle = "Categories" } htmlMatches
+
+-- | Returns username of logged in user or null string if nobody logged in.
+currentUser :: Handler
+currentUser = do
+  req <- askRq
+  ok $ toResponse $ maybe "" toString (getHeader "REMOTE_USER" req)
+
+expireCache :: Handler
+expireCache = do
+  page <- getPage
+  -- try it as a page first, then as an uploaded file
+  expireCachedFile (pathForPage page) `mplus` expireCachedFile page
+  ok $ toResponse ()
+
+feedHandler :: Handler
+feedHandler = do
+  cfg <- getConfig
+  when (not $ useFeed cfg) mzero
+  base' <- getWikiBase
+  feedBase <- if null (baseUrl cfg)  -- if baseUrl blank, try to get it from Host header
+                 then do
+                   mbHost <- getHost
+                   case mbHost of
+                        Nothing    -> error "Could not determine base URL"
+                        Just hn    -> return ("http://" ++ hn ++ base')
+                 else return (baseUrl cfg ++ base')
+  let fc = FeedConfig{
+              fcTitle = wikiTitle cfg
+            , fcBaseUrl = feedBase
+            , fcFeedDays = feedDays cfg }
+  path' <- getPath     -- e.g. "foo/bar" if they hit /_feed/foo/bar
+  let file = (path' `orIfNull` "_site") <.> "feed"
+  let mbPath = if null path' then Nothing else Just path'
+  fs <- getFileStore
+  -- first, check for a cached version that is recent enough
+  now <- liftIO $ getClockTime
+  let isRecentEnough t = diffClockTimes now t > noTimeDiff{tdMin = fromIntegral $ feedRefreshTime cfg}
+  mbCached <- lookupCache file
+  case mbCached of
+       Just (modtime, contents) | isRecentEnough modtime -> do
+            let emptyResponse = setContentType "application/atom+xml; charset=utf-8" . toResponse $ ()
+            ok $ emptyResponse{rsBody = B.fromChunks [contents]}
+       _ -> do
+            resp <- liftM toResponse $ liftIO (filestoreToXmlFeed fc fs mbPath)
+            cacheContents file $ S.concat $ B.toChunks $ rsBody resp
+            ok . setContentType "application/atom+xml; charset=UTF-8" $ resp
diff --git a/Network/Gitit/Initialize.hs b/Network/Gitit/Initialize.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Initialize.hs
@@ -0,0 +1,185 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- | Functions for initializing a Gitit wiki.
+-}
+
+module Network.Gitit.Initialize ( initializeGititState
+                                , recompilePageTemplate
+                                , createStaticIfMissing
+                                , createRepoIfMissing
+                                , createTemplateIfMissing )
+where
+import System.FilePath ((</>), (<.>))
+import Data.FileStore
+import qualified Data.Map as M
+import Network.Gitit.Types
+import Network.Gitit.State
+import Network.Gitit.Framework
+import Network.Gitit.Plugins
+import Network.Gitit.Layout (defaultRenderPage)
+import Paths_gitit (getDataFileName)
+import Control.Exception (throwIO, try)
+import System.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, doesFileExist)
+import Control.Monad (unless, forM_, liftM)
+import Prelude hiding (readFile)
+import System.IO.UTF8
+import Text.Pandoc
+import Text.Pandoc.Shared (HTMLMathMethod(..))
+import System.Log.Logger (logM, Priority(..))
+import qualified Text.StringTemplate as T
+
+-- | Initialize Gitit State.
+initializeGititState :: Config -> IO ()
+initializeGititState conf = do
+  let userFile' = userFile conf
+      pluginModules' = pluginModules conf
+  plugins' <- loadPlugins pluginModules'
+
+  userFileExists <- doesFileExist userFile'
+  users' <- if userFileExists
+               then liftM (M.fromList . read) $ readFile userFile'
+               else return M.empty
+
+  templ <- compilePageTemplate (templatesDir conf)
+
+  updateGititState $ \s -> s { sessions      = Sessions M.empty
+                             , users         = users'
+                             , templatesPath = templatesDir conf
+                             , renderPage    = defaultRenderPage templ
+                             , plugins       = plugins' }
+
+-- | Recompile the page template.
+recompilePageTemplate :: IO ()
+recompilePageTemplate = do
+  tempsDir <- queryGititState templatesPath
+  ct <- compilePageTemplate tempsDir
+  updateGititState $ \st -> st{renderPage = defaultRenderPage ct}
+
+-- | Compile a master page template named @page.st@ in the directory specified.
+compilePageTemplate :: FilePath -> IO (T.StringTemplate String)
+compilePageTemplate tempsDir = do
+  defaultGroup <- getDataFileName ("data" </> "templates") >>= T.directoryGroup
+  templateExists <- doesDirectoryExist tempsDir
+  customGroup <- if templateExists
+                    then T.directoryGroup tempsDir
+                    else return T.nullGroup
+  -- default templates from data directory will be "shadowed"
+  -- by templates from the user's template dir
+  let combinedGroup = T.addSubGroup defaultGroup customGroup
+  case T.getStringTemplate "page" combinedGroup of
+        Just t    -> return t
+        Nothing   -> error "Could not get string template"
+ 
+-- | Create templates dir if it doesn't exist.
+createTemplateIfMissing :: Config -> IO ()
+createTemplateIfMissing conf' = do
+  templateExists <- doesDirectoryExist (templatesDir conf')
+  unless templateExists $ do
+    createDirectoryIfMissing True (templatesDir conf')
+    templatePath <- getDataFileName $ "data" </> "templates"
+    -- templs <- liftM (filter (`notElem` [".",".."])) $
+    --  getDirectoryContents templatePath
+    -- Copy footer.st, since this is the component users
+    -- are most likely to want to customize:
+    forM_ ["footer.st"] $ \t -> do
+      copyFile (templatePath </> t) (templatesDir conf' </> t)
+      logM "gitit" WARNING $ "Created " ++ (templatesDir conf' </> t)
+
+-- | Create page repository unless it exists.
+createRepoIfMissing :: Config -> IO ()
+createRepoIfMissing conf = do
+  let fs = filestoreFromConfig conf
+  repoExists <- try (initialize fs) >>= \res ->
+    case res of
+         Right _               -> do
+           logM "gitit" WARNING $ "Created repository in " ++ repositoryPath conf
+           return False
+         Left RepositoryExists -> return True
+         Left e                -> throwIO e >> return False
+  let pt = defaultPageType conf
+  let toPandoc = readMarkdown
+                 defaultParserState{ stateSanitizeHTML = True
+                                   , stateSmart = True }
+  let defOpts = defaultWriterOptions{
+                        writerStandalone = False
+                      , writerHTMLMathMethod = JsMath
+                               (Just "/js/jsMath/easy/load.js")
+                      , writerLiterateHaskell = showLHSBirdTracks conf
+                      }
+  -- note: we convert this (markdown) to the default page format
+  let converter = case defaultPageType conf of
+                     Markdown -> id
+                     LaTeX    -> writeLaTeX defOpts . toPandoc
+                     HTML     -> writeHtmlString defOpts . toPandoc
+                     RST      -> writeRST defOpts . toPandoc
+  unless repoExists $ do
+    welcomepath <- getDataFileName $ "data" </> "FrontPage" <.> "page"
+    welcomecontents <- liftM converter $ readFile welcomepath
+    helppath <- getDataFileName $ "data" </> "Help" <.> "page"
+    helpcontentsInitial <- liftM converter $ readFile helppath
+    markuppath <- getDataFileName $ "data" </> "markup" <.> show pt
+    helpcontentsMarkup <- liftM converter $ readFile markuppath
+    let helpcontents = helpcontentsInitial ++ "\n\n" ++ helpcontentsMarkup
+    usersguidepath <- getDataFileName "README.markdown"
+    usersguidecontents <- liftM converter $ readFile usersguidepath
+    -- add front page, help page, and user's guide
+    create fs (frontPage conf <.> "page") (Author "Gitit" "") "Default front page" welcomecontents
+    logM "gitit" WARNING $ "Added " ++ (frontPage conf <.> "page") ++ " to repository"
+    create fs "Help.page" (Author "Gitit" "") "Default help page" helpcontents
+    logM "gitit" WARNING $ "Added " ++ "Help.page" ++ " to repository"
+    create fs "Gitit User's Guide.page" (Author "Gitit" "") "User's guide (README)" usersguidecontents
+    logM "gitit" WARNING $ "Added " ++ "Gitit User's Guide.page" ++ " to repository"
+
+-- | Create static directory unless it exists.
+createStaticIfMissing :: Config -> IO ()
+createStaticIfMissing conf = do
+  let staticdir = staticDir conf
+  staticExists <- doesDirectoryExist staticdir
+  unless staticExists $ do
+
+    let cssdir = staticdir </> "css"
+    createDirectoryIfMissing True cssdir
+    cssDataDir <- getDataFileName $ "data" </> "static" </> "css"
+    -- cssFiles <- liftM (filter (\f -> takeExtension f == ".css")) $ getDirectoryContents cssDataDir
+    forM_ ["custom.css"] $ \f -> do
+      copyFile (cssDataDir </> f) (cssdir </> f)
+      logM "gitit" WARNING $ "Created " ++ (cssdir </> f)
+
+    {-
+    let icondir = staticdir </> "img" </> "icons" 
+    createDirectoryIfMissing True icondir
+    iconDataDir <- getDataFileName $ "data" </> "static" </> "img" </> "icons"
+    iconFiles <- liftM (filter (\f -> takeExtension f == ".png")) $ getDirectoryContents iconDataDir
+    forM_ iconFiles $ \f -> do
+      copyFile (iconDataDir </> f) (icondir </> f)
+      logM "gitit" WARNING $ "Created " ++ (icondir </> f)
+    -}
+
+    logopath <- getDataFileName $ "data" </> "static" </> "img" </> "gitit-dog.png"
+    createDirectoryIfMissing True $ staticdir </> "img"
+    copyFile logopath $ staticdir </> "img" </> "logo.png"
+    logM "gitit" WARNING $ "Created " ++ (staticdir </> "img" </> "logo.png")
+
+    {-
+    let jsdir = staticdir </> "js"
+    createDirectoryIfMissing True jsdir
+    jsDataDir <- getDataFileName $ "data" </> "static" </> "js"
+    javascripts <- liftM (filter (`notElem` [".", ".."])) $ getDirectoryContents jsDataDir
+    forM_ javascripts $ \f -> do
+      copyFile (jsDataDir </> f) (jsdir </> f)
+      logM "gitit" WARNING $ "Created " ++ (jsdir </> f)
+    -}
+
diff --git a/Network/Gitit/Interface.hs b/Network/Gitit/Interface.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Interface.hs
@@ -0,0 +1,169 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- | Interface for plugins.
+
+A plugin is a Haskell module that is dynamically loaded by gitit.
+
+There are three kinds of plugins: 'PageTransform's,
+'PreParseTransform's, and 'PreCommitTransform's. These plugins differ
+chiefly in where they are applied. 'PreCommitTransform' plugins are
+applied just before changes to a page are saved and may transform
+the raw source that is saved. 'PreParseTransform' plugins are applied
+when a page is viewed and may alter the raw page source before it
+is parsed as a 'Pandoc' document. Finally, 'PageTransform' plugins
+modify the 'Pandoc' document that results after a page's source is
+parsed, but before it is converted to HTML:
+
+>                 +--------------------------+
+>                 | edited text from browser |
+>                 +--------------------------+
+>                              ||         <----  PreCommitTransform plugins
+>                              \/
+>                              ||         <----  saved to repository
+>                              \/
+>              +---------------------------------+
+>              | raw page source from repository |
+>              +---------------------------------+
+>                              ||         <----  PreParseTransform plugins
+>                              \/
+>                              ||         <----  markdown or RST reader
+>                              \/
+>                     +-----------------+
+>                     | Pandoc document |
+>                     +-----------------+
+>                              ||         <---- PageTransform plugins
+>                              \/
+>                   +---------------------+
+>                   | new Pandoc document |
+>                   +---------------------+
+>                              ||         <---- HTML writer
+>                              \/
+>                   +----------------------+
+>                   | HTML version of page |
+>                   +----------------------+
+
+Note that 'PreParseTransform' and 'PageTransform' plugins do not alter
+the page source stored in the repository. They only affect what is
+visible on the website.  Only 'PreCommitTransform' plugins can
+alter what is stored in the repository.
+
+Note also that 'PreParseTransform' and 'PageTransform' plugins will
+not be run when the cached version of a page is used.  Plugins can
+use the 'doNotCache' command to prevent a page from being cached,
+if their behavior is sensitive to things that might change from
+one time to another (such as the time or currently logged-in user).
+
+You can use the helper functions 'mkPageTransform' and 'mkPageTransformM'
+to create 'PageTransform' plugins from a transformation of any
+of the basic types used by Pandoc (for example, @Inline@, @Block@,
+@[Inline]@, even @String@). Here is a simple (if silly) example:
+
+> -- Deprofanizer.hs
+> module Deprofanizer (plugin) where
+>
+> -- This plugin replaces profane words with "XXXXX".
+>
+> import Network.Gitit.Interface
+> import Data.Char (toLower)
+>
+> plugin :: Plugin
+> plugin = mkPageTransform deprofanize
+>
+> deprofanize :: Inline -> Inline
+> deprofanize (Str x) | isBadWord x = Str "XXXXX"
+> deprofanize x                     = x
+>
+> isBadWord :: String -> Bool
+> isBadWord x = (map toLower x) `elem` ["darn", "blasted", "stinker"]
+> -- there are more, but this is a family program
+
+Further examples can be found in the @plugins@ directory in
+the source distribution.  If you have installed gitit using Cabal,
+you can also find them in the directory
+@CABALDIR\/share\/gitit-X.Y.Z\/plugins@, where @CABALDIR@ is the cabal
+install directory and @X.Y.Z@ is the version number of gitit.
+
+-}
+
+module Network.Gitit.Interface ( Plugin(..)
+                               , PluginM
+                               , mkPageTransform
+                               , mkPageTransformM
+                               , Config(..)
+                               , Request(..)
+                               , User(..)
+                               , Context(..)
+                               , PageType(..)
+                               , PageLayout(..)
+                               , askConfig
+                               , askUser
+                               , askRequest
+                               , askFileStore
+                               , doNotCache
+                               , getContext
+                               , modifyContext
+                               , inlinesToURL
+                               , inlinesToString
+                               , liftIO
+                               , withTempDir
+                               , module Text.Pandoc.Definition
+                               )
+where
+import Text.Pandoc.Definition
+import Data.Data
+import Network.Gitit.Types
+import Network.Gitit.ContentTransformer
+import Network.Gitit.Util (withTempDir)
+import Network.Gitit.Server (Request(..))
+import Control.Monad.Reader (ask)
+import Control.Monad.Trans (liftIO)
+import Control.Monad (liftM)
+import Data.FileStore (FileStore)
+
+-- | Returns the current wiki configuration.
+askConfig :: PluginM Config
+askConfig = liftM pluginConfig ask
+
+-- | Returns @Just@ the logged in user, or @Nothing@ if nobody is logged in.
+askUser :: PluginM (Maybe User)
+askUser = liftM pluginUser ask
+
+-- | Returns the complete HTTP request.
+askRequest :: PluginM Request
+askRequest = liftM pluginRequest ask
+
+-- | Returns the wiki filestore.
+askFileStore :: PluginM FileStore
+askFileStore = liftM pluginFileStore ask
+
+-- | Indicates that the current page or file is not to be cached.
+doNotCache :: PluginM ()
+doNotCache = modifyContext (\ctx -> ctx{ ctxCacheable = False })
+
+-- | Lifts a function from @a -> a@ (for example, @Inline -> Inline@,
+-- @Block -> Block@, @[Inline] -> [Inline]@, or @String -> String@)
+-- to a 'PageTransform' plugin.
+mkPageTransform :: Data a => (a -> a) -> Plugin
+mkPageTransform fn = PageTransform $ return . processWith fn
+
+-- | Monadic version of 'mkPageTransform'.
+-- Lifts a function from @a -> m a@ to a 'PageTransform' plugin.
+mkPageTransformM :: Data a => (a -> PluginM a) -> Plugin
+mkPageTransformM =  PageTransform . processWithM
+
diff --git a/Network/Gitit/Layout.hs b/Network/Gitit/Layout.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Layout.hs
@@ -0,0 +1,145 @@
+{-# LANGUAGE FlexibleContexts #-}
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- Functions and data structures for wiki page layout.
+-}
+
+module Network.Gitit.Layout ( defaultPageLayout
+                            , defaultRenderPage
+                            , formattedPage
+                            )
+where
+import Network.Gitit.Server
+import Network.Gitit.Framework
+import Network.Gitit.State
+import Network.Gitit.Types
+import Network.Gitit.Export (exportFormats)
+import Network.HTTP (urlEncodeVars)
+import qualified Text.StringTemplate as T
+import Prelude hiding (catch)
+import Text.XHtml hiding ( (</>), dir, method, password, rev )
+import Text.XHtml.Strict ( stringToHtmlString )
+import Data.Maybe (isNothing, isJust, fromJust)
+
+defaultPageLayout :: PageLayout
+defaultPageLayout = PageLayout
+  { pgPageName       = ""
+  , pgRevision       = Nothing
+  , pgPrintable      = False
+  , pgMessages       = []
+  , pgTitle          = ""
+  , pgScripts        = []
+  , pgShowPageTools  = True
+  , pgShowSiteNav    = True
+  , pgMarkupHelp     = Nothing
+  , pgTabs           = [ViewTab, EditTab, HistoryTab, DiscussTab]
+  , pgSelectedTab    = ViewTab
+  , pgLinkToFeed     = False
+  }
+
+-- | Returns formatted page
+formattedPage :: PageLayout -> Html -> Handler
+formattedPage layout htmlContents = do
+  renderer <- queryGititState renderPage
+  renderer layout htmlContents
+
+-- | Given a compiled string template, returns a page renderer.
+defaultRenderPage :: T.StringTemplate String -> PageLayout -> Html -> Handler
+defaultRenderPage templ layout htmlContents = do
+  cfg <- getConfig
+  let rev  = pgRevision layout
+  let page = pgPageName layout
+  base' <- getWikiBase
+  let scripts  = ["jquery.min.js", "jquery-ui.packed.js"] ++ pgScripts layout
+  let scriptLink x = script ! [src (base' ++ "/js/" ++ x),
+        thetype "text/javascript"] << noHtml
+  let javascriptlinks = renderHtmlFragment $ concatHtml $ map scriptLink scripts
+  let tabli tab = if tab == pgSelectedTab layout
+                     then li ! [theclass "selected"]
+                     else li
+  let tabs' = [x | x <- pgTabs layout,
+                not (x == EditTab && page `elem` noEdit cfg)]
+  let tabs = ulist ! [theclass "tabs"] << map (linkForTab tabli base' page rev) tabs'
+  let setStrAttr  attr = T.setAttribute attr . stringToHtmlString
+  let setBoolAttr attr test = if test then T.setAttribute attr "true" else id
+  let filledTemp = T.render .
+                   T.setAttribute "base" base' .
+                   T.setAttribute "feed" (pgLinkToFeed layout) .
+                   setStrAttr "pagetitle" (pgTitle layout) .
+                   T.setAttribute "javascripts" javascriptlinks .
+                   setStrAttr "pagename" page .
+                   setStrAttr "pageUrl" (urlForPage page) .
+                   setBoolAttr "ispage" (isPage page) .
+                   setBoolAttr "pagetools" (pgShowPageTools layout) .
+                   setBoolAttr "sitenav" (pgShowSiteNav layout) .
+                   maybe id (T.setAttribute "markuphelp") (pgMarkupHelp layout) .
+                   setBoolAttr "printable" (pgPrintable layout) .
+                   maybe id (T.setAttribute "revision") rev .
+                   T.setAttribute "exportbox"
+                       (renderHtmlFragment $  exportBox base' page rev) .
+                   T.setAttribute "tabs" (renderHtmlFragment tabs) .
+                   T.setAttribute "messages" (pgMessages layout) .
+                   T.setAttribute "usecache" (useCache cfg) .
+                   T.setAttribute "content" (renderHtmlFragment htmlContents) $
+                   templ
+  ok $ setContentType "text/html" $ toResponse filledTemp
+
+exportBox :: String -> String -> Maybe String -> Html
+exportBox base' page rev | not (isSourceCode page) =
+  gui (base' ++ urlForPage page) ! [identifier "exportbox"] <<
+    ([ textfield "revision" ! [thestyle "display: none;",
+         value (fromJust rev)] | isJust rev ] ++
+     [ select ! [name "format"] <<
+         map ((\f -> option ! [value f] << f) . fst) exportFormats
+     , submit "export" "Export" ])
+exportBox _ _ _ = noHtml
+
+-- auxiliary functions:
+
+linkForTab :: (Tab -> Html -> Html) -> String -> String -> Maybe String -> Tab -> Html
+linkForTab tabli base' page _ HistoryTab =
+  tabli HistoryTab << anchor ! [href $ base' ++ "/_history" ++ urlForPage page] << "history"
+linkForTab tabli _ _ _ DiffTab =
+  tabli DiffTab << anchor ! [href ""] << "diff"
+linkForTab tabli base' page rev ViewTab =
+  let origPage s = if isDiscussPage s
+                      then drop 1 s
+                      else s
+  in if isDiscussPage page
+        then tabli DiscussTab << anchor !
+              [href $ base' ++ urlForPage (origPage page)] << "page"
+        else tabli ViewTab << anchor !
+              [href $ base' ++ urlForPage page ++
+                      case rev of
+                           Just r  -> "?revision=" ++ r
+                           Nothing -> ""] << "view"
+linkForTab tabli base' page _ DiscussTab =
+  tabli (if isDiscussPage page then ViewTab else DiscussTab) <<
+  anchor ! [href $ base' ++ if isDiscussPage page then "" else "/_discuss" ++
+                   urlForPage page] << "discuss"
+linkForTab tabli base' page rev EditTab =
+  tabli EditTab << anchor !
+    [href $ base' ++ "/_edit" ++ urlForPage page ++
+            case rev of
+                  Just r   -> "?revision=" ++ r ++ "&" ++
+                               urlEncodeVars [("logMsg", "Revert to " ++ r)]
+                  Nothing  -> ""] << if isNothing rev
+                                         then "edit"
+                                         else "revert"
+
diff --git a/Network/Gitit/Page.hs b/Network/Gitit/Page.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Page.hs
@@ -0,0 +1,145 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- Functions for translating between Page structures and raw
+-  text strings.  The strings may begin with a metadata block,
+-  which looks like this (it is valid YAML):
+-
+-  > ---
+-  > title: Custom Title 
+-  > format: markdown+lhs
+-  > toc: yes
+-  > categories: foo bar baz
+-  > ...
+-
+-  This would tell gitit to use "Custom Title" as the displayed
+-  page title (instead of the page name), to interpret the page
+-  text as markdown with literate haskell, to include a table of
+-  contents, and to include the page in the categories foo, bar,
+-  and baz.
+- 
+-  The metadata block may be omitted entirely, and any particular line
+-  may be omitted. The categories in the @categories@ field should be
+-  separated by spaces. Commas will be treated as spaces.
+-
+-  Metadata value fields may be continued on the next line, as long as
+-  it is nonblank and starts with a space character.
+-
+-  Unrecognized metadata fields are simply ignored.
+-}
+
+module Network.Gitit.Page ( stringToPage
+                          , pageToString
+                          , extractCategories
+                          )
+where
+import Network.Gitit.Types
+import Network.Gitit.Util (trim, splitCategories, parsePageType)
+import Text.ParserCombinators.Parsec
+import Data.Char (toLower)
+import Data.List (intercalate)
+import Data.Maybe (fromMaybe)
+
+parseMetadata :: String -> ([(String, String)], String)
+parseMetadata raw =
+  case parse pMetadataBlock "" raw of
+    Left _           -> ([], raw)
+    Right (ls, rest) -> (ls, rest)
+
+pMetadataBlock :: GenParser Char st ([(String, String)], String)
+pMetadataBlock = try $ do
+  string "---"
+  pBlankline
+  ls <- many pMetadataLine
+  string "..."
+  pBlankline
+  skipMany pBlankline
+  rest <- getInput
+  return (ls, rest)
+
+pBlankline :: GenParser Char st Char
+pBlankline = try $ many (oneOf " \t") >> newline
+
+pMetadataLine :: GenParser Char st (String, String)
+pMetadataLine = try $ do
+  ident <- many1 letter
+  skipMany (oneOf " \t")
+  char ':'
+  rawval <- many $ noneOf "\n\r"
+                 <|> (try $ newline >> notFollowedBy pBlankline >>
+                            skipMany1 (oneOf " \t") >> return ' ')
+  newline
+  return (ident, trim rawval)
+
+-- | Read a string (the contents of a page file) and produce a Page
+-- object, using defaults except when overridden by metadata.
+stringToPage :: Config -> String -> String -> Page
+stringToPage conf pagename raw =
+  let (ls, rest) = parseMetadata raw
+      page' = Page { pageName        = pagename 
+                   , pageFormat      = defaultPageType conf
+                   , pageLHS         = defaultLHS conf
+                   , pageTOC         = tableOfContents conf
+                   , pageTitle       = pagename
+                   , pageCategories  = []
+                   , pageText        = filter (/= '\r') rest }
+  in  foldr adjustPage page' ls
+
+adjustPage :: (String, String) -> Page -> Page
+adjustPage ("title", val) page' = page' { pageTitle = val }
+adjustPage ("format", val) page' = page' { pageFormat = pt, pageLHS = lhs }
+    where (pt, lhs) = parsePageType val
+adjustPage ("toc", val) page' = page' {
+  pageTOC = (map toLower val) `elem` ["yes","true"] }
+adjustPage ("categories", val) page' =
+   page' { pageCategories = splitCategories val ++ pageCategories page' }
+adjustPage (_, _) page' = page'
+
+-- | Write a string (the contents of a page file) corresponding to
+-- a Page object, using explicit metadata only when needed.
+pageToString :: Config -> Page -> String
+pageToString conf page' =
+  let pagename   = pageName page'
+      pagetitle  = pageTitle page'
+      pageformat = pageFormat page'
+      pagelhs    = pageLHS page'
+      pagetoc    = pageTOC page'
+      pagecats   = pageCategories page'
+      metadata'  = (if pagename /= pagetitle
+                       then "!title: " ++ pagetitle ++ "\n"
+                       else "") ++
+                   (if pageformat /= defaultPageType conf ||
+                          pagelhs /= defaultLHS conf
+                       then "!format: " ++ 
+                            map toLower (show pageformat) ++
+                            if pagelhs then "+lhs\n" else "\n"
+                       else "") ++
+                   (if pagetoc /= tableOfContents conf
+                       then "!toc: " ++
+                            (if pagetoc then "yes" else "no") ++ "\n"
+                       else "") ++
+                   (if not (null pagecats)
+                       then "!categories: " ++ intercalate " " pagecats ++ "\n"
+                       else "")
+  in  metadata' ++ (if null metadata' then "" else "\n") ++ pageText page'
+
+extractCategories :: String -> [String]
+extractCategories s | take 3 s == "---" = 
+  let (md,_) = parseMetadata s
+  in  splitCategories $ fromMaybe "" $ lookup "categories" md
+extractCategories _ = []
diff --git a/Network/Gitit/Plugins.hs b/Network/Gitit/Plugins.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Plugins.hs
@@ -0,0 +1,79 @@
+{-# LANGUAGE CPP #-}
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- Functions for loading plugins.
+-}
+
+module Network.Gitit.Plugins ( loadPlugin, loadPlugins )
+where
+import Network.Gitit.Types
+import System.FilePath
+import Control.Monad (unless)
+import System.Log.Logger (logM, Priority(..))
+#ifdef _PLUGINS
+import Data.List (isInfixOf, isPrefixOf)
+import GHC
+import GHC.Paths
+import DynFlags
+import Unsafe.Coerce
+
+loadPlugin :: FilePath -> IO Plugin
+loadPlugin pluginName = do
+  logM "gitit" WARNING ("Loading plugin '" ++ pluginName ++ "'...")
+  defaultCleanupHandler defaultDynFlags $
+    runGhc (Just libdir) $ do
+      dflags <- getSessionDynFlags
+      setSessionDynFlags dflags
+      unless ("Network.Gitit.Plugin." `isPrefixOf` pluginName)
+        $ do
+            addTarget =<< guessTarget pluginName Nothing
+            r <- load LoadAllTargets
+            case r of
+              Failed -> error $ "Error loading plugin: " ++ pluginName
+              Succeeded -> return ()
+      let modName =
+            if "Network.Gitit.Plugin" `isPrefixOf` pluginName
+               then pluginName
+               else if "Network/Gitit/Plugin/" `isInfixOf` pluginName
+                       then "Network.Gitit.Plugin." ++ takeBaseName pluginName
+                       else takeBaseName pluginName
+      pr <- findModule (mkModuleName "Prelude") Nothing
+      i <- findModule (mkModuleName "Network.Gitit.Interface") Nothing
+      m <- findModule (mkModuleName modName) Nothing
+      setContext [] [m, i, pr]
+      value <- compileExpr (modName ++ ".plugin :: Plugin")
+      let value' = (unsafeCoerce value) :: Plugin
+      return value'
+
+#else
+
+loadPlugin :: FilePath -> IO Plugin
+loadPlugin pluginName = do
+  error $ "Cannot load plugin '" ++ pluginName ++
+          "'. gitit was not compiled with plugin support."
+  return undefined
+
+#endif
+
+loadPlugins :: [FilePath] -> IO [Plugin]
+loadPlugins pluginNames = do
+  plugins' <- mapM loadPlugin pluginNames
+  unless (null pluginNames) $ logM "gitit" WARNING "Finished loading plugins."
+  return plugins'
+
diff --git a/Network/Gitit/Server.hs b/Network/Gitit/Server.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Server.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE CPP #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+{-
+Copyright (C) 2008 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- Re-exports Happstack functions needed by gitit, including
+   replacements for Happstack functions that don't handle UTF-8 properly, and
+   new functions for setting headers and zipping contents and for looking up IP
+   addresses.
+-}
+
+module Network.Gitit.Server
+          ( module Happstack.Server
+          , withExpiresHeaders
+          , setContentType
+          , setFilename
+          , lookupIPAddr
+          , getHost
+          , compressedResponseFilter
+          )
+where
+import Happstack.Server
+import Happstack.Server.Parts (compressedResponseFilter)
+import Network.Socket (getAddrInfo, defaultHints, addrAddress)
+import Control.Monad.Reader
+import Data.Maybe
+import Data.ByteString.UTF8 as U
+
+withExpiresHeaders :: ServerMonad m => m Response -> m Response
+withExpiresHeaders = liftM (setHeader "Cache-Control" "max-age=21600")
+
+setContentType :: String -> Response -> Response
+setContentType = setHeader "Content-Type"
+
+setFilename :: String -> Response -> Response
+setFilename = setHeader "Content-Disposition" . \fname -> "attachment; filename=\"" ++ fname ++ "\""
+
+-- IP lookup
+
+lookupIPAddr :: String -> IO (Maybe String)
+lookupIPAddr hostname = do
+  addrs <- getAddrInfo (Just defaultHints) (Just hostname) Nothing
+  if null addrs
+     then return Nothing
+     else return $ Just $ takeWhile (/=':') $ show $ addrAddress $ head addrs
+
+getHost :: ServerMonad m => m (Maybe String)
+getHost = liftM (maybe Nothing (Just . U.toString)) $ getHeaderM "Host"
diff --git a/Network/Gitit/State.hs b/Network/Gitit/State.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/State.hs
@@ -0,0 +1,140 @@
+{-
+Copyright (C) 2008 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- Functions for maintaining user list and session state.
+-}
+
+module Network.Gitit.State where
+
+import qualified Data.Map as M
+import System.Random (randomRIO)
+import Data.Digest.Pure.SHA (sha512, showDigest)
+import qualified Data.ByteString.Lazy.UTF8 as L (fromString)
+import qualified System.IO.Cautious as C (writeFile)
+import Data.IORef
+import System.IO.Unsafe (unsafePerformIO)
+import Control.Monad.Reader
+import Data.FileStore
+import Data.List (intercalate)
+import System.Log.Logger (Priority(..), logM)
+import Network.Gitit.Types
+
+gititstate :: IORef GititState
+gititstate = unsafePerformIO $  newIORef  GititState { sessions = undefined
+                                                     , users = undefined
+                                                     , templatesPath = undefined
+                                                     , renderPage = undefined
+                                                     , plugins = undefined }
+
+updateGititState :: MonadIO m => (GititState -> GititState) -> m ()
+updateGititState fn = liftIO $! atomicModifyIORef gititstate $ \st -> (fn st, ())
+
+queryGititState :: MonadIO m => (GititState -> a) -> m a
+queryGititState fn = liftM fn $ liftIO $! readIORef gititstate
+
+debugMessage :: String -> GititServerPart ()
+debugMessage msg = liftIO $ logM "gitit" DEBUG msg
+
+mkUser :: String   -- username
+       -> String   -- email
+       -> String   -- unhashed password
+       -> IO User
+mkUser uname email pass = do
+  salt <- genSalt
+  return  User { uUsername = uname,
+                 uPassword = Password { pSalt = salt,
+                                        pHashed = hashPassword salt pass },
+                 uEmail = email }
+
+genSalt :: IO String
+genSalt = replicateM 32 $ randomRIO ('0','z')
+
+hashPassword :: String -> String -> String
+hashPassword salt pass = showDigest $ sha512 $ L.fromString $ salt ++ pass
+
+authUser :: String -> String -> GititServerPart Bool
+authUser name pass = do
+  users' <- queryGititState users
+  case M.lookup name users' of
+       Just u  -> do
+         let salt = pSalt $ uPassword u
+         let hashed = pHashed $ uPassword u
+         return $ hashed == hashPassword salt pass
+       Nothing -> return False
+
+isUser :: String -> GititServerPart Bool
+isUser name = liftM (M.member name) $ queryGititState users
+
+addUser :: String -> User -> GititServerPart ()
+addUser uname user =
+  updateGititState (\s -> s { users = M.insert uname user (users s) }) >>
+  getConfig >>=
+  liftIO . writeUserFile
+
+adjustUser :: String -> User -> GititServerPart ()
+adjustUser uname user = updateGititState
+  (\s -> s  { users = M.adjust (const user) uname (users s) }) >>
+  getConfig >>=
+  liftIO . writeUserFile
+
+delUser :: String -> GititServerPart ()
+delUser uname =
+  updateGititState (\s -> s { users = M.delete uname (users s) }) >>
+  getConfig >>=
+  liftIO . writeUserFile
+
+writeUserFile :: Config -> IO ()
+writeUserFile conf = do
+  usrs <- queryGititState users
+  C.writeFile (userFile conf) $
+      "[" ++ intercalate "\n," (map show $ M.toList usrs) ++ "\n]"
+
+getUser :: String -> GititServerPart (Maybe User)
+getUser uname = liftM (M.lookup uname) $ queryGititState users
+
+isSession :: MonadIO m => SessionKey -> m Bool
+isSession key = liftM (M.member key . unsession) $ queryGititState sessions
+
+setSession :: MonadIO m => SessionKey -> SessionData -> m ()
+setSession key u = updateGititState $ \s ->
+  s { sessions = Sessions . M.insert key u . unsession $ sessions s }
+
+newSession :: MonadIO m => SessionData -> m SessionKey
+newSession u = do
+  key <- liftIO $ randomRIO (0, 1000000000)
+  setSession key u
+  return key
+
+delSession :: MonadIO m => SessionKey -> m ()
+delSession key = updateGititState $ \s ->
+  s { sessions = Sessions . M.delete key . unsession $ sessions s }
+
+getSession :: MonadIO m => SessionKey -> m (Maybe SessionData)
+getSession key = queryGititState $ M.lookup key . unsession . sessions
+
+getConfig :: GititServerPart Config
+getConfig = liftM wikiConfig ask
+
+getFileStore :: GititServerPart FileStore
+getFileStore = liftM wikiFileStore ask
+
+getDefaultPageType :: GititServerPart PageType
+getDefaultPageType = liftM defaultPageType getConfig
+
+getDefaultLHS :: GititServerPart Bool
+getDefaultLHS = liftM defaultLHS getConfig
diff --git a/Network/Gitit/Types.hs b/Network/Gitit/Types.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Types.hs
@@ -0,0 +1,365 @@
+{-# LANGUAGE TypeSynonymInstances #-}
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>,
+Anton van Straaten <anton@appsolutions.com>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- | Types for Gitit modules.
+-}
+
+module Network.Gitit.Types where
+
+import Control.Monad.Reader (ReaderT, runReaderT, mplus)
+import Control.Monad.State (StateT, runStateT, get, modify)
+import Control.Monad (liftM)
+import System.Log.Logger (Priority(..))
+import Text.Pandoc.Definition (Pandoc)
+import Text.XHtml (Html)
+import qualified Data.ByteString.Lazy.UTF8 as L (ByteString)
+import qualified Data.ByteString.Lazy as L (empty)
+import qualified Data.Map as M
+import Data.List (intersect)
+import Data.DateTime
+import Data.Maybe (fromMaybe)
+import Data.FileStore.Types
+import Network.Gitit.Server
+import Text.Pandoc.CharacterReferences (decodeCharacterReferences)
+
+data PageType = Markdown | RST | LaTeX | HTML
+                deriving (Read, Show, Eq)
+
+data FileStoreType = Git | Darcs deriving Show
+
+data MathMethod = MathML | JsMathScript | RawTeX
+                  deriving (Read, Show, Eq)
+
+-- | Data structure for information read from config file.
+data Config = Config {
+  -- | Path of repository containing filestore
+  repositoryPath       :: FilePath,
+  -- | Type of repository
+  repositoryType       :: FileStoreType,
+  -- | Default page markup type for this wiki
+  defaultPageType      :: PageType,
+  -- | How to handle LaTeX math in pages?
+  mathMethod           :: MathMethod,
+  -- | Treat as literate haskell by default?
+  defaultLHS           :: Bool,
+  -- | Show Haskell code with bird tracks
+  showLHSBirdTracks    :: Bool,
+  -- | Combinator to set @REMOTE_USER@ request header
+  withUser             :: Handler -> Handler,
+  -- | Handler for login, logout, register, etc.
+  authHandler          :: Handler,
+  -- | Path of users database
+  userFile             :: FilePath,
+  -- | Directory containing page templates
+  templatesDir         :: FilePath,
+  -- | Path of server log file
+  logFile              :: FilePath,
+  -- | Severity filter for log messages (DEBUG, INFO,
+  -- NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY)
+  logLevel             :: Priority,
+  -- | Path of static directory
+  staticDir            :: FilePath,
+  -- | Names of plugin modules to load
+  pluginModules        :: [String],
+  -- | Show table of contents on each page?
+  tableOfContents      :: Bool,
+  -- | Max size of pages and file uploads
+  maxUploadSize        :: Integer,
+  -- | Port number to serve content on
+  portNumber           :: Int,
+  -- | Print debug info to the console?
+  debugMode            :: Bool,
+  -- | The front page of the wiki
+  frontPage            :: String,
+  -- | Pages that cannot be edited via web
+  noEdit               :: [String],
+  -- | Pages that cannot be deleted via web
+  noDelete             :: [String],
+  -- | Default summary if description left blank
+  defaultSummary       :: String,
+  -- | @Nothing@ = anyone can register.
+  -- @Just (prompt, answers)@ = a user will
+  -- be given the prompt and must give
+  -- one of the answers to register.
+  accessQuestion       :: Maybe (String, [String]),
+  -- | Use ReCAPTCHA for user registration.
+  useRecaptcha         :: Bool,
+  recaptchaPublicKey   :: String,
+  recaptchaPrivateKey  :: String,
+  -- | Should responses be compressed?
+  compressResponses    :: Bool,
+  -- | Should responses be cached?
+  useCache             :: Bool,
+  -- | Directory to hold cached pages
+  cacheDir             :: FilePath,
+  -- | Map associating mime types with file extensions
+  mimeMap              :: M.Map String String,
+  -- | Command to send notification emails
+  mailCommand          :: String,
+  -- | Text of password reset email
+  resetPasswordMessage :: String,
+  -- | Markup syntax help for edit sidebar
+  markupHelp           :: String,
+  -- | Provide an atom feed?
+  useFeed              :: Bool,
+  -- | Base URL of wiki, for use in feed
+  baseUrl              :: String,
+  -- | Title of wiki, used in feed
+  wikiTitle            :: String,
+  -- | Number of days history to be included in feed
+  feedDays             :: Integer,
+  -- | Number of minutes to cache feeds before refreshing
+  feedRefreshTime      :: Integer
+  }
+
+-- | Data for rendering a wiki page.
+data Page = Page {
+    pageName        :: String
+  , pageFormat      :: PageType
+  , pageLHS         :: Bool
+  , pageTOC         :: Bool
+  , pageTitle       :: String
+  , pageCategories  :: [String]
+  , pageText        :: String
+} deriving (Read, Show)
+
+type SessionKey = Integer
+
+data SessionData = SessionData {
+  sessionUser :: String
+} deriving (Read,Show,Eq)
+
+data Sessions a = Sessions {unsession::M.Map SessionKey a}
+  deriving (Read,Show,Eq)
+
+-- Password salt hashedPassword
+data Password = Password { pSalt :: String, pHashed :: String }
+  deriving (Read,Show,Eq)
+
+data User = User {
+  uUsername :: String,
+  uPassword :: Password,
+  uEmail    :: String
+} deriving (Show,Read)
+
+-- | Common state for all gitit wikis in an application.
+data GititState = GititState {
+  sessions       :: Sessions SessionData,
+  users          :: M.Map String User,
+  templatesPath  :: FilePath,
+  renderPage     :: PageLayout -> Html -> Handler,
+  plugins        :: [Plugin]
+}
+
+type ContentTransformer = StateT Context GititServerPart
+
+data Plugin = PageTransform (Pandoc -> PluginM Pandoc)
+            | PreParseTransform (String -> PluginM String)
+            | PreCommitTransform (String -> PluginM String)
+
+data PluginData = PluginData { pluginConfig    :: Config
+                             , pluginUser      :: Maybe User
+                             , pluginRequest   :: Request
+                             , pluginFileStore :: FileStore
+                             }
+
+type PluginM = ReaderT PluginData (StateT Context IO)
+
+runPluginM :: PluginM a -> PluginData -> Context -> IO (a, Context)
+runPluginM plugin = runStateT . runReaderT plugin
+
+data Context = Context { ctxFile            :: String
+                       , ctxLayout          :: PageLayout
+                       , ctxCacheable       :: Bool
+                       , ctxTOC             :: Bool
+                       , ctxBirdTracks      :: Bool
+                       , ctxCategories      :: [String]
+                       }
+
+class (Monad m) => HasContext m where
+  getContext    :: m Context
+  modifyContext :: (Context -> Context) -> m ()
+
+instance HasContext ContentTransformer where
+  getContext    = get
+  modifyContext = modify
+
+instance HasContext PluginM where
+  getContext    = get
+  modifyContext = modify
+
+-- | Abstract representation of page layout (tabs, scripts, etc.)
+data PageLayout = PageLayout
+  { pgPageName       :: String
+  , pgRevision       :: Maybe String
+  , pgPrintable      :: Bool
+  , pgMessages       :: [String] 
+  , pgTitle          :: String
+  , pgScripts        :: [String]
+  , pgShowPageTools  :: Bool
+  , pgShowSiteNav    :: Bool
+  , pgMarkupHelp     :: Maybe String
+  , pgTabs           :: [Tab]
+  , pgSelectedTab    :: Tab
+  , pgLinkToFeed     :: Bool
+  }
+
+data Tab = ViewTab
+         | EditTab
+         | HistoryTab
+         | DiscussTab
+         | DiffTab
+         deriving (Eq, Show)
+
+data Recaptcha = Recaptcha {
+    recaptchaChallengeField :: String
+  , recaptchaResponseField  :: String
+  } deriving (Read, Show)
+
+instance FromData SessionKey where
+     fromData = readCookieValue "sid"
+
+data Params = Params { pUsername     :: String
+                     , pPassword     :: String
+                     , pPassword2    :: String
+                     , pRevision     :: Maybe String
+                     , pDestination  :: String
+                     , pForUser      :: Maybe String
+                     , pSince        :: Maybe DateTime
+                     , pRaw          :: String
+                     , pLimit        :: Int
+                     , pPatterns     :: [String]
+                     , pGotoPage     :: String
+                     , pFileToDelete :: String
+                     , pEditedText   :: Maybe String
+                     , pMessages     :: [String]
+                     , pFrom         :: Maybe String
+                     , pTo           :: Maybe String
+                     , pFormat       :: String
+                     , pSHA1         :: String
+                     , pLogMsg       :: String
+                     , pEmail        :: String
+                     , pFullName     :: String
+                     , pAccessCode   :: String
+                     , pWikiname     :: String
+                     , pPrintable    :: Bool
+                     , pOverwrite    :: Bool
+                     , pFilename     :: String
+                     , pFileContents :: L.ByteString
+                     , pConfirm      :: Bool 
+                     , pSessionKey   :: Maybe SessionKey
+                     , pRecaptcha    :: Recaptcha
+                     , pResetCode    :: String
+                     }  deriving Show
+
+instance FromData Params where
+     fromData = do
+         let look' = liftM decodeCharacterReferences . look
+         un <- look' "username"       `mplus` return ""
+         pw <- look' "password"       `mplus` return ""
+         p2 <- look' "password2"      `mplus` return ""
+         rv <- (look' "revision" >>= \s ->
+                 return (if null s then Nothing else Just s))
+                 `mplus` return Nothing
+         fu <- liftM Just (look' "forUser") `mplus` return Nothing
+         si <- liftM (parseDateTime "%Y-%m-%d") (look' "since")
+                 `mplus` return Nothing  -- YYYY-mm-dd format
+         ds <- look' "destination" `mplus` return ""
+         ra <- look' "raw"            `mplus` return ""
+         lt <- lookRead "limit"       `mplus` return 100
+         pa <- look' "patterns"       `mplus` return ""
+         gt <- look' "gotopage"       `mplus` return ""
+         ft <- look' "filetodelete"   `mplus` return ""
+         me <- lookRead "messages"   `mplus` return [] 
+         fm <- liftM Just (look' "from") `mplus` return Nothing
+         to <- liftM Just (look' "to")   `mplus` return Nothing
+         et <- liftM (Just . filter (/='\r')) (look' "editedText")
+                 `mplus` return Nothing
+         fo <- look' "format"         `mplus` return ""
+         sh <- look' "sha1"           `mplus` return ""
+         lm <- look' "logMsg"         `mplus` return ""
+         em <- look' "email"          `mplus` return ""
+         na <- look' "full_name_1"    `mplus` return ""
+         wn <- look' "wikiname"       `mplus` return ""
+         pr <- (look' "printable" >> return True) `mplus` return False
+         ow <- liftM (=="yes") (look' "overwrite") `mplus` return False
+         fn <- liftM (fromMaybe "" . inputFilename) (lookInput "file")
+                 `mplus` return ""
+         fc <- liftM inputValue (lookInput "file") `mplus` return L.empty
+         ac <- look' "accessCode"     `mplus` return ""
+         cn <- (look' "confirm" >> return True) `mplus` return False
+         sk <- liftM Just (readCookieValue "sid") `mplus` return Nothing
+         rc <- look' "recaptcha_challenge_field" `mplus` return ""
+         rr <- look' "recaptcha_response_field" `mplus` return ""
+         rk <- look' "reset_code" `mplus` return ""
+         return   Params { pUsername     = un
+                         , pPassword     = pw
+                         , pPassword2    = p2
+                         , pRevision     = rv
+                         , pForUser      = fu
+                         , pSince        = si
+                         , pDestination  = ds
+                         , pRaw          = ra
+                         , pLimit        = lt
+                         , pPatterns     = words pa
+                         , pGotoPage     = gt
+                         , pFileToDelete = ft
+                         , pMessages     = me
+                         , pFrom         = fm
+                         , pTo           = to
+                         , pEditedText   = et
+                         , pFormat       = fo 
+                         , pSHA1         = sh
+                         , pLogMsg       = lm
+                         , pEmail        = em
+                         , pFullName     = na 
+                         , pWikiname     = wn
+                         , pPrintable    = pr 
+                         , pOverwrite    = ow
+                         , pFilename     = fn
+                         , pFileContents = fc
+                         , pAccessCode   = ac
+                         , pConfirm      = cn
+                         , pSessionKey   = sk
+                         , pRecaptcha    = Recaptcha {
+                              recaptchaChallengeField = rc,
+                              recaptchaResponseField = rr }
+                         , pResetCode    = rk
+                         }
+
+data Command = Command (Maybe String) deriving Show
+
+instance FromData Command where
+     fromData = do
+       pairs <- lookPairs
+       return $ case map fst pairs `intersect` commandList of
+                 []          -> Command Nothing
+                 (c:_)       -> Command $ Just c
+               where commandList = ["update", "cancel", "export"]
+
+-- | State for a single wiki.
+data WikiState = WikiState { 
+                     wikiConfig    :: Config
+                   , wikiFileStore :: FileStore
+                   }
+
+type GititServerPart = ServerPartT (ReaderT WikiState IO)
+
+type Handler = GititServerPart Response
diff --git a/Network/Gitit/Util.hs b/Network/Gitit/Util.hs
new file mode 100644
--- /dev/null
+++ b/Network/Gitit/Util.hs
@@ -0,0 +1,90 @@
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+{- Utility functions for Gitit.
+-}
+
+module Network.Gitit.Util ( inDir
+                          , withTempDir
+                          , orIfNull
+                          , splitCategories
+                          , trim
+                          , yesOrNo
+                          , parsePageType
+                          )
+where
+import System.Directory
+import Control.Exception (bracket)
+import System.FilePath ((</>), (<.>))
+import System.IO.Error (isAlreadyExistsError)
+import Control.Monad.Trans (liftIO)
+import Data.Char (toLower)
+import Network.Gitit.Types
+
+-- | Perform a function a directory and return to working directory.
+inDir :: FilePath -> IO a -> IO a
+inDir d action = do
+  w <- getCurrentDirectory
+  setCurrentDirectory d
+  result <- action
+  setCurrentDirectory w
+  return result 
+
+-- | Perform a function in a temporary directory and clean up.
+withTempDir :: FilePath -> (FilePath -> IO a) -> IO a
+withTempDir baseName = bracket (createTempDir 0 baseName) (removeDirectoryRecursive)
+
+-- | Create a temporary directory with a unique name.
+createTempDir :: Integer -> FilePath -> IO FilePath
+createTempDir num baseName = do
+  sysTempDir <- getTemporaryDirectory
+  let dirName = sysTempDir </> baseName <.> show num
+  liftIO $ catch (createDirectory dirName >> return dirName) $
+      \e -> if isAlreadyExistsError e
+               then createTempDir (num + 1) baseName
+               else ioError e
+
+-- | Returns a list, if it is not null, or a backup, if it is.
+orIfNull :: [a] -> [a] -> [a]
+orIfNull lst backup = if null lst then backup else lst
+
+-- | Split a string containing a list of categories.
+splitCategories :: String -> [String]
+splitCategories = words . map puncToSpace . trim
+     where puncToSpace x | x `elem` ".,;:" = ' '
+           puncToSpace x = x
+
+-- | Trim leading and trailing spaces.
+trim :: String -> String
+trim = reverse . trimLeft . reverse . trimLeft
+  where trimLeft = dropWhile (`elem` " \t")
+
+-- | Show Bool as "yes" or "no".
+yesOrNo :: Bool -> String
+yesOrNo True  = "yes"
+yesOrNo False = "no"
+
+parsePageType :: String -> (PageType, Bool)
+parsePageType s =
+  case map toLower s of
+       "markdown"     -> (Markdown,False)
+       "markdown+lhs" -> (Markdown,True)
+       "rst"          -> (RST,False)
+       "rst+lhs"      -> (RST,True)
+       "html"         -> (HTML,False)
+       "latex"        -> (LaTeX,False)
+       "latex+lhs"    -> (LaTeX,True)
+       x              -> error $ "Unknown page type: " ++ x
+
diff --git a/README.markdown b/README.markdown
--- a/README.markdown
+++ b/README.markdown
@@ -1,22 +1,44 @@
 Gitit
 =====
 
-Gitit is a wiki program written in Haskell. It uses [HAppS][] for the
-web server and [pandoc][] for markup processing. Pages and uploaded
-files are stored in a [git][] or [darcs][] repository and may be modified either
-by using the VCS's command-line tools or through the wiki's web interface.
-By default, pandoc's extended version of markdown is used as a markup language,
-but reStructuredText can also be used.  Pages can be exported in a
-number of different formats, including LaTeX, RTF, OpenOffice ODT, and
-MediaWiki markup. Gitit can be configured to display TeX math (using
-[jsMath][]) and highlighted source code (using [highlighting-kate][]).
+Gitit is a wiki program written in Haskell. It uses [Happstack][]
+for the web server and [pandoc][] for markup processing. Pages and
+uploaded files are stored in a [git][] or [darcs][] repository and may
+be modified either by using the VCS's command-line tools or through the
+wiki's web interface. By default, pandoc's extended version of markdown
+is used as a markup language, but reStructuredText, LaTeX, or HTML
+can also be used. Pages can be exported in a number of different
+formats, including LaTeX, RTF, OpenOffice ODT, and MediaWiki markup.
+Gitit can be configured to display TeX math (using [texmath][]) and
+highlighted source code (using [highlighting-kate][]).
 
-[git]: http://git.or.cz  
+Other features include
+
+* plugins: dynamically loaded page transformations written in Haskell
+  (see "Network.Gitit.Interface")
+
+* categories
+
+* TeX math
+
+* syntax highlighting of source code files and code snippets (using
+  highlighting-kate)
+
+* caching
+
+* Atom feeds (site-wide and per-page)
+
+* a library, "Network.Gitit", that makes it simple to include a gitit
+  wiki in any happstack application
+
+You can see a running demo at <http://gitit.johnmacfarlane.net>.
+
+[git]: http://git.or.cz
 [darcs]: http://darcs.net
 [pandoc]: http://johnmacfarlane.net/pandoc
-[HAppS]: http://happs.org
-[jsMath]: http://www.math.union.edu/~dpvc/jsMath/
-[highlighting-kate]: http://johnmacfarlane.net/highlighting-kate/  
+[Happstack]: http://happstack.com
+[highlighting-kate]: http://johnmacfarlane.net/highlighting-kate/
+[texmath]: http://github.com/jgm/texmath/tree/master
 
 Getting started
 ===============
@@ -33,17 +55,10 @@
 [here]: http://www.haskell.org/ghc/
 [cabal-install]:  http://hackage.haskell.org/trac/hackage/wiki/CabalInstall
 [quick install]:  http://hackage.haskell.org/trac/hackage/wiki/CabalInstall#Quick Installation on Unix
-[pcre]:  http://www.pcre.org/ 
 
-If you want the syntax highlighting feature, you need to make sure
-that pandoc is compiled with support for it.  First, make sure your system
-has the [pcre][] library installed.  Then:
+Once you've got cabal-install, installing gitit is trivial:
 
     cabal update
-    cabal install -fhighlighting pandoc gitit
-
-If you don't care about highlighting support, you can just do:
-
     cabal install gitit
 
 These commands will install the latest released version of gitit.
@@ -63,168 +78,222 @@
 cabal-install executable directory (usually `~/.cabal/bin`). And make
 sure `~/.cabal/bin` is in your system path.
 
+Optional syntax highlighting support
+------------------------------------
+
+If pandoc was compiled with optional syntax highlighting support,
+this will be available in gitit too.  This feature is recommended
+if you plan to display source code on your wiki.
+
+Highlighting support requires the [pcre][] library, so make sure that
+is installed before continuing.
+
+[pcre]:  http://www.pcre.org/ 
+
+To install gitit with highlighting support, first ensure that pandoc
+is compiled with highlighting support, then install gitit as above:
+
+    cabal install pandoc -fhighlighting --reinstall
+    cabal install gitit
+
 Running gitit
 -------------
 
-To run gitit, you'll need [git][] in your system path. Check this by doing
-
-    git --version
+To run gitit, you'll need [git][] in your system path. (Or
+[darcs][], if you're using darcs to store the wiki data.)
 
-You should also make sure that you are using a UTF-8 locale.
-(To check this, type `locale`.)
+Gitit assumes that the page files (stored in the git repository) are
+encoded as UTF-8. Even page names may be UTF-8 if the file system
+supports this. So you should make sure that you are using a UTF-8 locale
+when running gitit. (To check this, type `locale`.)
 
-Switch to the directory where you want to run gitit.  This should be a directory
-where you have write access, since two directories, `static` and `wikidata`, and
-two files, `gitit-users` and `template.html`, will be created here. To
-start gitit, just type:
+Switch to the directory where you want to run gitit. This should be a
+directory where you have write access, since three directories, `static`,
+`templates`, and `wikidata`, and two files, `gitit-users` and `gitit.log`,
+will be created here. To start gitit, just type:
 
     gitit
 
 If all goes well, gitit will do the following:
 
  1.  Create a git repository, `wikidata`, and add a default front page.
- 2.  Create a `static` directory containing the scripts and CSS used by gitit.
- 3.  Create a `template.html` file containing an (HStringTemplate) template
+ 2.  Create a `static` directory containing files to be treated as
+     static files by gitit.
+ 3.  Create a `templates` directory containing HStringTemplate templates
      for wiki pages.
  4.  Start a web server on port 5001.
 
-Check that it worked:  open a web browser and go to http://localhost:5001.
+Check that it worked: open a web browser and go to
+<http://localhost:5001>.
 
-Configuration options
----------------------
+Using gitit
+===========
 
-You can set some configuration options when starting gitit, using the
-option `-f [filename]`.  A configuration file takes the following form:
+Wiki links and formatting
+-------------------------
 
-    Config {
-    repository          = Git "wikidata",
-    defaultPageType     = Markdown,
-    userFile            = "gitit-users",
-    templateFile        = "template.html",
-    staticDir           = "static",
-    tableOfContents     = False,
-    maxUploadSize       = 100000,
-    portNumber          = 5001,
-    debugMode           = True,
-    frontPage           = "Front Page",
-    noEdit              = ["Help", "Front Page"],
-    noDelete            = ["Help", "Front Page"],
-    accessQuestion      = Just ("Enter the access code (to request an access code, contact me@somewhere.org):", ["abcd"]),
-    useRecaptcha        = False,
-    recaptchaPublicKey  = "",
-    recaptchaPrivateKey = "",
-    mimeTypesFile       = "/etc/mime.types"
-    }
+For instructions on editing pages and creating links, see the "Help" page.
 
-- `repository` specifies the type and (relative) path of the repository
-  in which the wiki's pages will be stored. If it does not exist, gitit
-  will create it on startup.  Supported repository types are `Git` and
-  `Darcs`.
+Gitit interprets links with empty URLs as wikilinks. Thus, in markdown
+pages, `[Front Page]()` creates an internal wikilink to the page `Front
+Page`. In reStructuredText pages, `` `Front Page <>`_ `` has the same
+effect.
 
-- `defaultPageType` is the type of markup used to interpret pages in
-  the wiki. Two values are currently supported: `Markdown` and `RST`.
-  If `Markdown` is selected, [pandoc]'s syntax extensions (for footnotes,
-  delimited code blocks, etc.) will be enabled.  Note that pandoc's
-  reStructuredText parser is not complete, so some pages may
-  not be rendered correctly if `RST` is selected.
+If you want to link to a directory listing for a subdirectory, use a
+trailing slash:  `[foo/bar/]()` creates a link to the directory for
+`foo/bar`.
 
-- `userFile` is a file containing user login information (with hashed
-  passwords).  If it does not exist, gitit will start with an empty list
-  of users.  Gitit will write a new `userFile` on shutdown.
+Page metadata
+-------------
 
-- `templateFile` is a file containing an HTML template for the wiki pages.
-  If it does not exist, gitit will create a default template.  (For most
-  purposes, this can be used just as it is, but some users may wish to
-  customize the look of their wiki.)  `templateFile` is an
-  `HStringTemplate` template.
+Pages may optionally begin with a metadata block.  Here is an example:
 
-- `staticDir` is the (relative) path of a directory in which static content
-  (javascript, CSS, images) is stored.  If it does not exist, gitit will
-  create it on startup.
+    ---
+    format: latex+lhs
+    categories: haskell math
+    toc: no
+    title: Haskell and
+      Category Theory
+    ...
 
-- `tableOfContents` is either `False` or `True`.  If it is `True`, a table
-  of contents (derived from the page's headers) will appear on each page.
+    \section{Why Category Theory?}
 
-- `maxUploadSize` (in bytes) sets a limit to the size of file uploads.
+The metadata block consists of a list of key-value pairs, each on a
+separate line. If needed, the value can be continued on one or more
+additional line, which must begin with a space. (This is illustrated by
+the "title" example above.) The metadata block must begin with a line
+`---` and end with a line `...` optionally followed by one or more blank
+lines. (The metadata block is a valid YAML document, though not all YAML
+documents will be valid metadata blocks.)
 
-- `portNumber` is the number of the port on which the wiki will be served.
+Currently the following keys are supported:
 
-- `debugMode` is either `True` or `False`. If it is `True`, debug information
-  will be printed to the console when gitit is running.
+format
+:   Overrides the default page type as specified in the configuration file.
+    Possible values are `markdown`, `rst`, `latex`, `html`, `markdown+lhs`,
+    `rst+lhs`, `latex+lhs`.  (Capitalization is ignored, so you can also
+    use `LaTeX`, `HTML`, etc.)  The `+lhs` variants indicate that the page
+    is to be interpreted as literate Haskell.  If this field is missing,
+    the default page type will be used.
 
-- `frontPage` is the name of the page that is designated as the "front" or
-  "entrance" page of the wiki.  Any page may be designated.
+categories
+:   A space or comma separated list of categories to which the page belongs.
 
-- `noEdit` is a list of pages that cannot be edited.
+toc
+:   Overrides default setting for table-of-contents in the configuration file.
+    Values can be `yes`, `no`, `true`, or `false` (capitalization is ignored).
 
-- `noDelete` is a list of pages that cannot be deleted.
+title
+:   By default the displayed page title is the page name.  This metadata element
+    overrides that default.
 
-- `accessQuestion` provides primitive access control.  It is either `Nothing`,
-  in which case anyone will be allowed to create an account and edit wiki pages,
-  or `Just (question, [answer1, answer2, ...])`, where question is a prompt
-  that will be displayed when a user tries to create an account, and
-  `answer1, answer2, ...` are the valid responses. The user must provide a
-  valid response in order to create an account. 
+Highlighted source code
+-----------------------
 
-- `useRecaptcha` is either `True` or `False`. It specifies whether to
-  use the [reCAPTCHA] service to provide captchas for user registration.
+If gitit was compiled against a version of pandoc that has highlighting
+support (see above), you can get highlighted source code by using
+[delimited code blocks][]:
 
-- `recaptchaPublicKey` and `recaptchaPrivateKey` are
-  [reCAPTCHA] keys, which can be obtained free of charge at
-  <http://recaptcha.net/api/getkey>.  The values of these fields are ignored
-  if `useRecaptcha` is set to `False`.
+    ~~~ {.haskell .numberLines}
+    qsort []     = []
+    qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
+                   qsort (filter (>= x) xs) 
+    ~~~
 
-- `mimeTypesFile` is the path of a file containing mime type associations.
-  Each line of the file should contain a mime type, followed by some space,
-  followed by a space-separated list of file extensions that map to that mime
-  type.  If the file is not found, some simple defaults will be used.
+To see what languages are available:
 
-[reCAPTCHA]: http://recaptcha.net
+    pandoc -v
 
-Configuring gitit
-=================
+[delimited code blocks]: http://johnmacfarlane.net/pandoc/README.html#delimited-code-blocks
 
+Configuring and customizing gitit
+=================================
+
+Configuration options
+---------------------
+
+You can set some configuration options when starting gitit, using the
+option `-f [filename]`. To get a copy of the default configuration file,
+which you can customize, just type:
+
+    gitit --print-default-config > default.conf
+
+The default configuration file is documented with comments throughout.
+
 The `static` directory
 ----------------------
 
-If there is no wiki page or uploaded file corresponding to a request, gitit
-always looks last in the `static` directory. So, for example, a file
-`foo.jpg` in the `img` subdirectory of the `static` directory will be
-accessible at the url `/img/foo.jpg`. Pandoc creates three subdirectories
-of `static`, `css`, `img`, and `js`, which include the icons, stylesheets,
-and javascripts it uses.
+On receiving a request, gitit always looks first in the `static`
+directory (or in whatever directory is specified for `static-dir` in
+the configuration file). If a file corresponding to the request is
+found there, it is served immediately. If the file is not found in
+`static`, gitit next looks in the `static` subdirectory of gitit's data
+file (`$CABALDIR/share/gitit-x.y.z/data`). This is where default css,
+images, and javascripts are stored. If the file is not found there
+either, gitit treats the request as a request for a wiki page or wiki
+command.
 
-Note:  if you set `staticDir` to be a subdirectory of `repositoryPath`,
+So, you can throw anything you want to be served statically (for
+example, a `robots.txt` file or `favicon.ico`) in the `static`
+directory. You can override any of gitit's default css, javascript, or
+image files by putting a file with the same relative path in `static`.
+Note that gitit has a default `robots.txt` file that excludes all
+URLs beginning with `/_`.
+
+Note:  if you set `static-dir` to be a subdirectory of `repository-path`,
 and then add the files in the static directory to your repository, you
 can ensure that others who clone your wiki repository get these files
 as well.  It will not be possible to modify these files using the web
 interface, but they will be modifiable via git.
 
+Using darcs instead of git
+--------------------------
+
+By default, gitit will store wiki pages in a git repository in the
+`wikidata` directory.  If you'd prefer to use darcs instead of git,
+you need to add the following field to the configuration file:
+
+    repository-type: Darcs
+
+This program may be called "darcsit" instead of "gitit" when a darcs
+backend is used.
+
+Note:  we recommend that you use gitit/darcsit with darcs version
+2.3.0 or greater.  If you must use an older version of darcs, then
+you need to compile the filestore library without the (default)
+maxcount flag, before (re)installing gitit:
+
+    cabal install --reinstall filestore -f-maxcount
+    cabal install --reinstall gitit
+
+Otherwise you will get an error when you attempt to access your
+repository.
+
 Changing the theme
 ------------------
 
-To change the look of the wiki, modify `screen.css` in `static/css`.
-To change the look of printed pages, modify `print.css`.
+To change the look of the wiki, you can modify `custom.css` in
+`static/css`.
+
+To change the look of printed pages, copy gitit's default `print.css`
+to `static/css` and modify it.
+
 The logo picture can be changed by copying a new PNG file to
-`static/img/logo.png`. For more radical changes, one can modify
-`template.html`.
+`static/img/logo.png`.
 
+To change the footer, modify `templates/footer.st`.
+
+For more radical changes, you can override any of the default
+templates in `$CABALDIR/share/gitit-x.y.z/templates` by copying
+the file into `templates` and modifying it. The `page.st` template is
+the master template; it includes the others. Interpolated variables are
+surrounded by `$`s, so `literal $` must be backslash-escaped.
+
 Adding support for math
 -----------------------
 
-Gitit is designed to work with [jsMath][] to display LaTeX math in HTML. 
-Download `jsMath` and `jsMath Image Fonts` from the [jsMath download page][].
-You'll have two `.zip` archives. Unzip them both in the
-`static/js` directory (a new subdirectory, `jsMath`, will be
-created).  You can test to see if math is working properly by clicking
-"help" on the top navigation bar and looking for the math example
-(the quadratic formula).  Note that if you copied the `jsMath` directory
-into `static` *after* starting gitit, you will have to restart gitit
-for the change to be noticed.  Gitit checks for the existence of the
-jsMath files when it starts, and will not include links to them unless
-they exist.
-
-To write math on a wiki page, just enclose it in dollar signs, as in LaTeX:
+To write math on a markdown-formatted wiki page, just enclose it
+in dollar signs, as in LaTeX:
 
     Here is a formula:  $\frac{1}{\sqrt{c^2}}$
 
@@ -232,33 +301,52 @@
 
     $$\frac{1}{\sqrt{c^2}}$$
 
-[jsMath download page]: http://sourceforge.net/project/showfiles.php?group_id=172663
+Gitit can display TeX math in three different ways, depending on the
+setting of `math` in the configuration file:
 
-Highlighted source code
------------------------
+1.  `mathml` (default): Math will be converted to MathML using
+    [texmath][]. This method works with IE+mathplayer, Firefox, and
+    Opera, but not Safari.
 
-If gitit was compiled against a version of pandoc that has highlighting support
-(see above), you can get highlighted source code by using [delimited code blocks][]:
+2.  `jsMath`: Math will be rendered using the [jsMath][] javascript.
+    If you want to use this method, download `jsMath` and `jsMath
+    Image Fonts` from the [jsMath download page][]. You'll have two
+    `.zip` archives. Unzip them both in the `static/js` directory (a new
+    subdirectory, `jsMath`, will be created).  This works with all
+    browsers, but is slower and not as nice looking as MathML.
 
-    ~~~ {.haskell .numberLines}
-    qsort []     = []
-    qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
-                   qsort (filter (>= x) xs) 
-    ~~~
+3.  `raw`: Math will be rendered as raw LaTeX codes.
 
-To see what languages are available:
+[jsMath download page]: http://sourceforge.net/project/showfiles.php?group_id=172663
 
-    pandoc -v
+Plugins
+=======
 
-[delimited code blocks]: http://johnmacfarlane.net/pandoc/README.html#delimited-code-blocks
+Plugins are small Haskell programs that transform a wiki page after it
+has been converted from Markdown or RST. See the example plugins in the
+`plugins` directory. To enable a plugin, include the path to the plugin
+(or its module name) in the `plugins` field of the configuration file.
+(If the plugin name starts with `Network.Gitit.Plugin.`, gitit will assume that
+the plugin is an installed module and will not look for a source file.)
 
-Accessing the wiki via git
-==========================
+Plugin support is enabled by default. However, plugin support makes
+the gitit executable considerably larger and more memory-hungry.
+If you don't need plugins, you may want to compile gitit without plugin
+support.  To do this, unset the `plugins` Cabal flag:
 
-All the pages and uploaded files are stored in a git repository. By default, this
-lives in the `wikidata` directory (though this can be changed through configuration
-options).  So you can interact with the wiki using git command line tools:
+    cabal install --reinstall gitit -f-plugins
 
+Note also that if you compile gitit for executable profiling, attempts
+to load plugins will result in "internal error: PAP object entered!"
+
+Accessing the wiki via git or darcs
+===================================
+
+All the pages and uploaded files are stored in a git or darcs
+repository. By default, this lives in the `wikidata` directory (though
+this can be changed through configuration options). So you can interact
+with the wiki using git command line tools:
+
     git clone ssh://my.server.edu/path/of/wiki/wikidata
     cd wikidata
     vim Front\ Page.page  # edit the page
@@ -268,36 +356,182 @@
 If you now look at the Front Page on the wiki, you should see your changes
 reflected there.  Note that the pages all have the extension `.page`.
 
-Wiki links and formatting
-=========================
+Caching
+=======
 
-For instructions on editing pages and creating links, see the "Help" page.
+By default, gitit does not cache content.  If your wiki receives a lot of
+traffic or contains pages that are slow to render, you may want to activate
+caching.  To do this, set the configuration option `use-cache` to `yes`.
+By default, rendered pages and highlighted source files will be cached
+in the `cache` directory. (Another directory can be specified by setting
+the `cache-dir` configuration option.)
 
-Gitit interprets links with empty URLs as wikilinks.  Thus, in markdown pages,
-`[Front Page]()` creates an internal wikilink to the page `Front Page`.
-In reStructuredText pages, `` `Front Page <>`_ `` has the same effect.
+Cached pages are updated when pages are modified using the web
+interface. They are not updated when pages are modified directly through
+git or darcs. However, the cache can be refreshed manually by pressing
+Ctrl-R when viewing a page, or by sending an HTTP GET or POST request to
+`/_expire/path/to/page`, where `path/to/page` is the name of the page to
+be expired.
 
-Character encodings
-===================
+Users who frequently update pages using git or darcs may wish to add a
+hook to the repository that makes the appropriate HTTP request to expire
+pages when they are updated. To facilitate such hooks, the gitit cabal
+package includes an executable `expireGititCache`. Assuming you are
+running gitit at port 5001 on localhost, and the environment variable
+`CHANGED_FILES` contains a list of the files that have changed, you can
+expire their cached versions using
 
-Gitit assumes that the page files (stored in the git repository) are
-encoded as UTF-8.  Even page names may be UTF-8 if the file system supports
-this.  You should use a UTF-8 locale when running gitit.
+    expireGititCache http://localhost:5001 $CHANGED_FILES
 
+Or you can specify the files directly:
+
+    expireGititCache http://localhost:5001 "Front Page.page" foo/bar/baz.c
+
+This program will return a success status (0) if the page has been
+successfully expired (or if it was never cached in the first place),
+and a failure status (> 0) otherwise.
+
+The cache is persistent through restarts of gitit.  To expire all cached
+pages, simply remove the `cache` directory.
+
+Using gitit with apache
+=======================
+
+Most users who run a public-facing gitit will want gitit to appear
+at a nice URL like `http://wiki.mysite.com` or
+`http://mysite.com/wiki` rather than `http://mysite.com:5001`.
+This can be achieved using apache's `mod_proxy`.
+
+Proxying to `http://wiki.mysite.com`
+------------------------------------
+
+Set up your DNS so that `http://wiki.mysite.com` maps to
+your server's IP address. Make sure that the `mod_proxy` module is
+loaded, and set up a virtual host with the following configuration:
+
+    <VirtualHost *>
+        ServerName wiki.mysite.com
+        DocumentRoot /var/www/
+        RewriteEngine On
+        ProxyPreserveHost On
+        ProxyRequests Off
+    
+        <Proxy *>
+           Order deny,allow
+           Allow from all
+        </Proxy>
+    
+        ProxyPassReverse /    http://127.0.0.1:5001
+        RewriteRule ^(.*) http://127.0.0.1:5001$1 [P]
+    
+        ErrorLog /var/log/apache2/error.log
+        LogLevel warn
+    
+        CustomLog /var/log/apache2/access.log combined
+        ServerSignature On
+    
+    </VirtualHost>
+
+Reload your apache configuration and you should be all set.
+
+Proxying to `http://mysite.com/wiki`
+------------------------------------
+
+Make sure the `mod_proxy`, `mod_headers`, `mod_proxy_http`,
+and `mod_proxy_html` modules are loaded. `mod_proxy_html`
+is an external module, which can be obtained [here]
+(http://apache.webthing.com/mod_proxy_html/). It rewrites URLs that
+occur in web pages. Here we will use it to rewrite gitit's links so that
+they all begin with `/wiki/`.
+
+First, tell gitit not to compress pages, since `mod_proxy_html` needs
+uncompressed pages to parse. You can do this by setting the gitit
+configuration option
+
+    compress-responses: no
+
+Second, modify the link in the `reset-password-message` in the
+configuration file:  instead of
+
+    http://$hostname$:$port$$resetlink$
+
+set it to
+
+    http://$hostname$/wiki$resetlink$
+
+Restart gitit.
+
+Now add the following lines to the apache configuration file for the
+`mysite.com` server:
+
+    # These commands will proxy /wiki/ to port 5001
+
+    ProxyRequests Off
+
+    <Proxy *>
+      Order deny,allow
+      Allow from all
+    </Proxy>
+
+    ProxyPass /wiki/ http://127.0.0.1:5001/
+
+    <Location /wiki/>
+      SetOutputFilter  proxy-html
+      ProxyPassReverse /
+      ProxyHTMLURLMap  /   /wiki/
+      RequestHeader unset Accept-Encoding
+    </Location>
+
+Reload your apache configuration and you should be set.
+
+For further information on the use of `mod_proxy_http` to rewrite URLs,
+see the [`mod_proxy_html` guide].
+
+[`mod_proxy_html` guide]: http://apache.webthing.com/mod_proxy_html/guide.html
+
+Using gitit as a library
+========================
+
+By importing the module `Network.Gitit`, you can include a gitit wiki
+(or several of them) in another happstack application. There are some
+simple examples in the haddock documentation for `Network.Gitit`.
+
 Reporting bugs
 ==============
 
 Bugs may be reported (and feature requests filed) at
 <http://code.google.com/p/gitit/issues/list>.
 
+There is a mailing list for users and developers at
+<http://groups.google.com/group/gitit-discuss>.
+
 Acknowledgements
 ================
 
-Gwern Branwen helped to optimize Gitit.  Simon Michael contributed the patch for
-RST support.
+A number of people have contributed patches:
 
-The visual layout is shamelessly borrowed from Wikipedia.
+- Gwern Branwen helped to optimize gitit and wrote the
+  InterwikiPlugin. He also helped with the Feed module.
+- Simon Michael contributed the patch adding RST support.
+- Henry Laxen added support for password resets and helped with
+  the apache proxy instructions.
+- Anton van Straaten made the process of page generation
+  more modular by adding Gitit.ContentTransformer.
+- Robin Green helped improve the plugin API and interface, and
+  fixed a security problem with the reset password code.
+- Thomas Hartman helped improve the index page, making directory
+  browsing persistent.
+- Justin Bogner improved the appearance of the preview button.
+- Kohei Ozaki contributed the ImgTexPlugin.
+- mightybyte suggested making gitit available as a library,
+  and contributed a patch to ifLoggedIn that was needed to
+  make gitit usable with a custom authentication scheme.
 
+I am especially grateful to the darcs team for using darcsit for
+their public-facing wiki.  This has helped immensely in identifying
+issues and improving performance.
+
+Gitit's default visual layout is shamelessly borrowed from Wikipedia.
 The stylesheets are influenced by Wikipedia's stylesheets and by the
 bluetrip CSS framework (see BLUETRIP-LICENSE). Some of the icons in
 `img/icons` come from bluetrip as well.
diff --git a/YUI-LICENSE b/YUI-LICENSE
new file mode 100644
--- /dev/null
+++ b/YUI-LICENSE
@@ -0,0 +1,30 @@
+Software License Agreement (BSD License)
+Copyright (c) 2009, Yahoo! Inc.
+All rights reserved.
+
+Redistribution and use of this software in source and binary forms,
+with or without modification, are permitted provided that the following
+conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * 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.
+
+    * Neither the name of Yahoo! Inc. nor the names of its contributors
+      may be used to endorse or promote products derived from this software
+      without specific prior written permission of Yahoo! Inc.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"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 COPYRIGHT
+OWNER OR CONTRIBUTORS 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.
diff --git a/css/hk-pyg.css b/css/hk-pyg.css
deleted file mode 100644
--- a/css/hk-pyg.css
+++ /dev/null
@@ -1,20 +0,0 @@
-/* Loosely based on pygment's default colors */
-table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode, table.sourceCode pre 
-   { margin: 0; padding: 0; border: 0; vertical-align: baseline; border: none; }
-td.lineNumbers { border-right: 1px solid #AAAAAA; text-align: right; color: #AAAAAA; padding-right: 5px; padding-left: 5px; } 
-td.sourceCode { padding-left: 5px; }
-pre.sourceCode { }
-pre.sourceCode span.Normal { }
-pre.sourceCode span.Keyword { color: #007020; font-weight: bold; } 
-pre.sourceCode span.DataType { color: #902000; }
-pre.sourceCode span.DecVal { color: #40a070; }
-pre.sourceCode span.BaseN { color: #40a070; }
-pre.sourceCode span.Float { color: #40a070; }
-pre.sourceCode span.Char { color: #4070a0; }
-pre.sourceCode span.String { color: #4070a0; }
-pre.sourceCode span.Comment { color: #60a0b0; font-style: italic; }
-pre.sourceCode span.Others { color: #007020; }
-pre.sourceCode span.Alert { color: red; font-weight: bold; }
-pre.sourceCode span.Function { color: #06287e; }
-pre.sourceCode span.RegionMarker { }
-pre.sourceCode span.Error { color: red; font-weight: bold; }
diff --git a/css/ie.css b/css/ie.css
deleted file mode 100644
--- a/css/ie.css
+++ /dev/null
@@ -1,20 +0,0 @@
-/* ie.css */
-body {text-align:center;}
-.container {text-align:left;}
-* html .column {overflow-x:hidden;}
-* html legend {margin:-18px -8px 16px 0;padding:0;}
-ol {margin-left:2em;}
-sup {vertical-align:text-top;}
-sub {vertical-align:text-bottom;}
-html>body p code {*white-space:normal;}
-hr {margin:-8px auto 11px;}
-.container ul { list-style: disc outside; margin-left: 2em; } /* IE can't handle :before and :after */
-.container ul li { text-indent: 0; margin-left: 0; }
-.container legend { margin-bottom: 1.6em; } /* IE form margin bug */
-sup, sub { font-size: 100%; } /* IE superscript & subscript bug */
-.container blockquote p, #content blockquote ul, #content blockquote ol, #content blockquote dl, #content blockquote pre, #content blockquote address, 
-.container blockquote table, #content blockquote form, #content blockquote h1, #content blockquote h2, #content blockquote h3, #content blockquote h4, #content blockquote h5, #content blockquote h6 { margin-top: .8em; margin-bottom: .8em; } /* IE can't handle :first-child */
-* html .container textarea, * html .container input { padding: 0; } /* IE < 7 form fix */
-.container input[type='submit'], .container input[type='button'] { padding: 0; } /* IE 7 button fix */
-.container legend+* { margin-top: 0; } /* we already added legend margin */
-a abbr, a acronym { text-decoration: underline; } /* IE 7 bug */
diff --git a/css/print.css b/css/print.css
deleted file mode 100644
--- a/css/print.css
+++ /dev/null
@@ -1,55 +0,0 @@
-/* -------------------------------------------------------------- 
-   gitit print css
-   borrows heavily from Mike Crittenden's BlueTripCSS framework (GPL)
-   and from Wikipedia's CSS.
--------------------------------------------------------------- */
-
-body { 
-width:100% !important;
-margin:0 !important;
-padding:0 !important;
-line-height: 1.4;
-word-spacing:1.1pt;
-letter-spacing:0.2pt; font-family: "Times New Roman", serif; color: #000; background: none; font-size: 12pt; }
-
-/*Headings */
-h1,h2,h3,h4,h5,h6 { font-family: Helvetica, Arial, sans-serif; }
-h1{font-size:19pt;}
-h2{font-size:17pt;}
-h3{font-size:15pt;}
-h4,h5,h6{font-size:12pt;}
-
-h2.revision { font-size: 10pt; font-weight: normal; font-style: italic; text-align: right; }
-
-pre, code { font: 10pt Courier, monospace; } 
-blockquote { margin: 1.3em; padding: 1em;  font-size: 10pt; }
-hr { background-color: #ccc; }
-
-/* Images */
-img { float: left; margin: 1em 1.5em 1.5em 0; }
-a img { border: none; }
-
-/* Links */
-a:link, a:visited { background: transparent; font-weight: normal; text-decoration: underline; color:#333; }
-a:link[href^="http://"]:after, a[href^="http://"]:visited:after { content: " (" attr(href) ")"; font-size: 90%; }
-a[href^="http://"] {color:#000; }
-
-/* Table */
-table { margin: 1px; text-align:left; }
-th { font-weight: bold; }
-th,td { padding: 4px 10px 4px 0; }
-tfoot { font-style: italic; }
-caption { background: #fff; margin-bottom:2em; text-align:left; }
-thead {display: table-header-group;}
-tr {page-break-inside: avoid;} 
-
-/*hide various parts from the site*/
-
-#maincol { margin-left: 1em; margin-right: 1em; border: none; }
-#content { border: none; }
-#sidebar, #userbox, #footer {display:none;}
-#toc { display: none; }
-h1 a:link, h2 a:link, h3 a:link, h4 a:link, h5 a:link, h6 a:link { text-decoration: none; }
-h1.pageTitle { font-size: 220%; }
-td.lineNumbers { display: none; }
-ul.tabs { display: none; }
diff --git a/css/screen.css b/css/screen.css
deleted file mode 100644
--- a/css/screen.css
+++ /dev/null
@@ -1,214 +0,0 @@
-@import url("/css/hk-pyg.css");
-
-/* gitit screen css - borrows heavily from Mike Crittenden's BlueTripCSS framework (GPL) and from Wikipedia's CSS. */
-
-/* MEYER RESET */
-html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, code, del, dfn, em, img, q, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td {margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;}
-body {line-height:1.5;}
-table {border-collapse:separate;border-spacing:0;}
-caption, th, td {text-align:left;font-weight:normal;}
-table, td, th {vertical-align:middle;}
-blockquote:before, blockquote:after, q:before, q:after {content:"";}
-blockquote, q {quotes:"" "";}
-a img {border:none;}
-
-/* BASIC TYPOGRAPHY */
-
-html { font-family: helvetica, "microsoft sans serif", arial, sans-serif; }
-strong, th, thead td, h1, h2, h3, h4, h5, h6 { font-weight: bold; }
-cite, em, dfn { font-style: italic; }
-code, kbd, samp, pre, tt, var, textarea { font-family: monospace; }
-del { text-decoration: line-through; color: #666; }
-ins, dfn { border-bottom: 1px solid #ccc; }
-small, sup, sub { font-size: 85%; }
-abbr, acronym { text-transform: uppercase; font-size: 85%; letter-spacing: .1em; }
-a abbr, a acronym { border: none; }
-abbr[title], acronym[title], dfn[title] { cursor: help; border-bottom: 1px solid #ccc; }
-sup { vertical-align: super; }
-sub { vertical-align: sub; }
-
-/* QUOTES */
-
-blockquote { border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; color: #666; }
-/* Uncomment these lines to have quotation marks inserted around block quotes: */
-/* blockquote *:first-child:before { content: "\201C"; } */
-/* blockquote *:first-child:after { content: "\201D"; } */
-
-/* FORMS */
-
-fieldset { padding:1.4em; margin: 0 0 1.5em 0; border: 1px solid #ccc; }
-legend { font-weight: bold; font-size:1.2em; }
-textarea, input[type='text'], input[type='password'], select { border: 1px solid #ccc; background: #fff; }
-textarea:hover, input[type='text']:hover, input[type='password']:hover, select:hover { border-color: #aaa; }
-textarea:focus, input[type='text']:focus, input[type='password']:focus, select:focus { border-color: #888; outline: 2px solid #ffffaa; }
-input, select { cursor: pointer; }
-input[type='text'] { cursor: text; }
-
-/* BASE SIZES */
-
-body { font-size: 1em; line-height: 1.6em; }
-
-hr { height: 1px; color: #aaa; background-color: #aaa; border: 0; margin: .2em 0 .2em 0;
-}
-
-h1, h2, h3, h4, h5, h6 { color: black; background: none; font-weight: normal; margin: 0;
-	padding-top: .5em; padding-bottom: .17em; border-bottom: 1px solid #aaa; }
-
-h1.pageTitle { font-size: 220%; margin: 0.2em 0 .5em;  }
-h1 { font-size: 150%; margin: 1.07em 0 .535em; }
-h2 { font-size: 132%; margin: 1.14em 0 .57em; }
-h3 { font-size: 116%; margin: 1.23em 0 .615em; }
-h4 { font-size: 100%; margin: 1.33em 0 .67em; }
-h5 { font-size: 88%; margin: 1.6em 0 .8em; }
-h6 { font-size: 80%; margin: 1.6em 0 .8em; }
-
-ul { line-height: 1.5em; list-style-type: square; margin: .3em 0 0 1.5em; padding: 0; }
-ol { line-height: 1.5em; margin: .3em 0 0 3.2em; padding: 0; }
-li { margin-bottom: .1em; }
-dt { font-weight: bold; margin-bottom: .1em; }
-dl { margin-top: .2em; margin-bottom: .5em; }
-dd { line-height: 1.5em; margin-left: 2em; margin-bottom: .1em; }
-
-/* TABLES */
-
-table { border-top: 1px solid #ccc;  border-left: 1px solid #ccc; }
-th, td { border-bottom: 1px solid #ddd; border-right: 1px solid #ccc; }
-
-/* MARGINS & PADDINGS */
-
-blockquote *:first-child { margin: .8em 0; }
-hr,  p,  ul,  ol,  dl,  blockquote,  address,  table,  pre, form { margin-bottom: 1.4em; }
-/* NOTE: Calulate header margins: TOP: 1.6em/size, BOTTOM: 1.6em/size/2 */
-th,  td { padding: .8em; }
-caption { padding-bottom: .8em; } /* padding instead of margin for IE */
-blockquote { padding: 0 1em; margin: 1.6em 0; }
-legend { padding-left: .8em; padding-right: .8em; }
-legend+* { margin-top: 1em; } /* compensates for the opera margin bug */
-textarea,  input { padding: .3em .4em .15em .4em; }
-select { padding: .1em .2em 0 .2em; }
-option { padding: 0 .4em; }
-a { position: relative; padding: 0.3em 0 .1em 0; } /* for larger click-area */
-dt { margin-top: .8em; margin-bottom: .4em; }
-ul { margin-left: 1.5em; }
-ol { margin-left: 2.35em; }
-ol ol,  ul ol { margin-left: 2.5em; }
-form div { margin-bottom: .8em; }
-
-/* COLORS */
-
-a:link { text-decoration: underline; color: #36c; }
-a:visited { text-decoration: underline; color: #99c; }
-a:hover { text-decoration: underline; color: #c33; }
-a:active,  a:focus { text-decoration: underline; color: #000; }
-/* code,  pre { color: #c33; } /* very optional, but still useful. W3C uses about the same colors for codes */
-
-/* TEXT CLASSES */
-
-.small {font-size:.8em;margin-bottom:1.875em;line-height:1.875em;}
-.large {font-size:1.2em;line-height:2.5em;margin-bottom:1.25em;}
-.hide {display:none;}
-.quiet {color:#666;}
-.loud {color:#000;}
-.highlight {background:#ff0;}
-.top {margin-top:0;padding-top:0;}
-.bottom {margin-bottom:0;padding-bottom:0;}
-.thin {font-weight: lighter;}
-.error,  .notice,  .success {padding:.8em;margin-bottom:1.6em;border:2px solid #ddd;}
-.error {background:#FBE3E4;color:#8a1f11;border-color:#FBC2C4;}
-.notice {background:#FFF6BF;color:#514721;border-color:#FFD324;}
-.success {background:#E6EFC2;color:#264409;border-color:#C6D880;}
-.error a {color:#8a1f11; background:none; padding:0; margin:0; }
-.notice a {color:#514721; background:none; padding:0; margin:0; }
-.success a {color:#264409; background:none; padding:0; margin:0; }
-.center {text-align: center;}
-
-input.search_term { width: 95% }
-
-/* Standard Buttons */
-button:hover, a.button:hover { background-color:#dff4ff; border:1px solid #c2e1ef; color:#336699; }
-a.button:active, button:active { background-color:#6299c5; border:1px solid #6299c5; color:#fff; }
-
-/* Link icons */
-
-/* Use this class if a link gets an icon when it shouldn't. */
-body a.noicon { background:none; padding:0; margin:0; }
-
-/* Make sure the icons are not cut */
-a[href^="http:"], a[href^="https:"], a[href^="mailto:"],
-a[href$=".pdf"], a[href$=".doc"], a[href$=".xls"], a[href$=".rss"],
-a[href$=".rdf"], a[href^="aim:"] {
-  padding:2px 22px 2px 0;
-  margin:-2px 0;
-  background-repeat: no-repeat;
-  background-position: right center;
-}
-
-/* External links */
-a[href^="http:"]          { background-image: url(../img/icons/external.png); padding-right: 14px; }
-a[href^="https:"]         { background-image: url(../img/icons/external.png); padding-right: 14px; }
-a[href^="mailto:"]        { background-image: url(../img/icons/email.png); }
-
-/* Files */
-a[href$=".pdf"]   { background-image: url(../img/icons/pdf.png); }
-a[href$=".doc"]   { background-image: url(../img/icons/doc.png); }
-a[href$=".xls"]   { background-image: url(../img/icons/xls.png); }
-
-/* Misc */
-a[href$=".rss"],
-a[href$=".rdf"]   { background-image: url(../img/icons/feed.png); }
-a[href^="aim:"]   { background-image: url(../img/icons/im.png); }
-
-h1 > a:link, h1 > a:active, h1 > a:hover, h1 > a:focus, h1 > a:visited,
-h2 > a:link, h2 > a:active, h2 > a:hover, h2 > a:focus, h2 > a:visited,
-h3 > a:link, h3 > a:active, h3 > a:hover, h3 > a:focus, h3 > a:visited,
-h4 > a:link, h4 > a:active, h4 > a:hover, h4 > a:focus, h4 > a:visited,
-h5 > a:link, h5 > a:active, h5 > a:hover, h5 > a:focus, h5 > a:visited,
-h6 > a:link, h6 > a:active, h6 > a:hover, h6 > a:focus, h6 > a:visited {
-        color: black;
-        text-decoration: none;
-}
-body { font: x-small sans-serif; background: #f9f9f9; color: black; margin: 0; padding: 0; }
-#container { font-size: 120%; margin: 0 ; padding: 0; }
-#userbox  { text-align: right; font-weight: bold; margin: 1em; }
-#logo { min-height: 50px; }
-#sidebar  { width: 12em; float: left; margin-right: 10px; }
-#sidebar fieldset { background-color: white; margin-bottom: 1em; padding: 0; }
-#sidebar fieldset, #sidebar fieldset legend { font-weight: normal; font-size: 95%; }
-#sidebar ul { padding: 0; margin: 0; margin-left: 1.6em; line-height: 1.5em; }
-#sidebar ul li { color: #888; }
-#maincol { position: absolute; margin-left: 13em; margin-top: 1em; padding-top: 0; margin-right: 0; max-width: 60em; }
-#content { border: 1px solid #ccc; background-color: #fff; padding: 1em; font-size: 110%; line-height: 150%; }
-div#toc { float: right; background-color: #f9f9f9; border: 10px solid white; margin: 0.8em; margin-right: 0; padding: 0.4em; }
-#toc ul { margin: 0; padding-left: 1em; list-style: none; }
-#toc > ul { margin-right: 1em; }
-/* .req is used to hide a honeypot in a form */
-.req { display: none; }
-ul.messages > li { color: red; list-style: square; font-weight: bold; }
-ul.tabs { padding: 0; margin: 0; }
-ul.tabs li { display: inline; border: 1px solid #ccc; border-bottom: none; border-collapse: collapse; padding: 0 0.6em 0 0.6em;
-    margin: 0 0 0 1.2em; overflow: visible; background: white; line-height: 1.2em; }
-ul.tabs li.selected { border-bottom: 3px solid white; }
-ul.tabs li a { text-decoration: none; font-size: 95%; font-weight: bold; margin: 0; z-index: 0; color: #36c; }
-.folding ul { list-style: none; margin: 0; padding: 0; }
-.folding li { list-style: none; background-position: 0 1px; background-repeat: no-repeat; padding-left: 20px; }
-.folding li.page { background-image: url(/img/icons/page.png); }
-.folding li.folder { background-image: url(/img/icons/folder.png); }
-.folding a { color: #000000; cursor: pointer; text-decoration: none; }
-.folding a:hover { text-decoration: underline; }
-#sidebar input, #sidebar select { font-size:  95%; padding: 0.1em; }
-#exportbox select { width: 8em; border: 1px solid #ccc; padding: 0; }
-#exportbox { margin: 0.3em 0 0.5em 0.4em; padding: 0; }
-#sidebar input[type='submit'] { border: none; background-color: #ccc; color: white; }
-#searchform { padding: 0; margin: 0.3em 0 0.5em 0.4em; }
-#searchform input[type='text'] { width: 7.5em; border: 1px solid #ccc; }
-#goform { padding: 0; margin: 0.3em 0 0.5em 0.4em; }
-#goform input[type='text'] { width: 7.5em; border: 1px solid #ccc; }
-.search_result { margin-bottom: 15px; }
-.search_result .match { line-height: 1em; margin-bottom: 15px; }
-pre.matches { font-size: .85em; margin: 0; padding: 0; }
-#editform textarea { height: 25em; width: 98%; }
-#editform #logMsg { width: 98%; margin-right: 1em; margin-bottom: 0.3em; }
-.added { background-color: yellow; }
-.deleted { text-decoration: line-through; color: gray; }
-h2.revision { font-size: 100%; color: #888; font-style: italic; border: none; margin: 0 0 0.5em 0; padding: 0; }
-#footer { padding: 1em; color: #888; text-align: center; font-size: 95%; }
diff --git a/data/FrontPage.page b/data/FrontPage.page
--- a/data/FrontPage.page
+++ b/data/FrontPage.page
@@ -1,37 +1,12 @@
 # Welcome to Gitit!
 
-Gitit is a wiki program written in Haskell. It uses [HAppS][] for the
-web server and [pandoc][] for markup processing. Pages and uploaded
-files are stored in a [git][] or [darcs][] repository and may be modified either
-by using the VCS's command-line tools or through the wiki's web interface.
-By default, pandoc's extended version of markdown is used as a markup language,
-but reStructuredText can also be used.  Pages can be exported in a
-number of different formats, including LaTeX, RTF, OpenOffice ODT, and
-MediaWiki markup. Gitit can be configured to display TeX math (using
-[jsMath][]) and highlighted source code (using [highlighting-kate][]).
-
-You can edit this page by double-clicking on it, or by clicking on the
-"edit" tab at the top of the screen.
-
-You can make a link to another wiki page like this:
-`[French Cheeses]()`.
-This will produce a link like this:  [French Cheeses]().  Note that
-the names of wiki pages need not be in CamelCase, and they may contain
-spaces.  Wiki pages may be organized into directories.  Use the
-slash ("/") character between directories and page names or
-subdirectories: `[Wines/Pinot Noir]()`.
-
-To create a new wiki page, just create a link to it and follow
-the link.
+This is the front page of your new gitit wiki.  You can edit this
+page by clicking on the "edit" tab at the top of the screen.
+For instructions on how to make a link to another wiki page, see [the
+Help page](Help#wiki-links). To create a new wiki page, just create a
+link to it and follow the link.
 
 Help is always available through the "Help" link in the sidebar.
-
-[git]: http://git.or.cz
-[darcs]: http://darcs.net
-[pandoc]: http://johnmacfarlane.net/pandoc
-[HAppS]: http://happs.org
-[jsMath]: http://www.math.union.edu/~dpvc/jsMath/
-[highlighting-kate]: http://johnmacfarlane.net/highlighting-kate/
-[Haskell]:  http://www.haskell.org/
-[markdown]: http://daringfireball.net/projects/markdown/
+More details on installing and configurating gitit are available
+in the [Gitit User's Guide]().
 
diff --git a/data/Help.page b/data/Help.page
--- a/data/Help.page
+++ b/data/Help.page
@@ -1,210 +1,14 @@
 # Navigating
 
 The most natural way of navigating is by clicking wiki links that
-connect one page with another. The "front" button on the top navigation
-bar will always take you to the Front Page of the wiki. The "index"
-button will take you to a list of all pages on the wiki (organized into
+connect one page with another. The "Front page" link in the navigation
+bar will always take you to the Front Page of the wiki. The "All pages"
+link will take you to a list of all pages on the wiki (organized into
 folders if directories are used). Alternatively, you can search using
 the search box. Note that the search is set to look for whole words, so
 if you are looking for "gremlins", type that and not "gremlin".
 The "go" box will take you directly to the page you type.
 
-# Markdown
-
-This wiki's pages are written in [pandoc]'s extended form of [markdown].
-If you're not familiar with markdown, you should start by looking
-at the [markdown "basics" page] and the [markdown syntax description].
-Consult the [pandoc User's Guide] for information about pandoc's syntax
-for footnotes, tables, description lists, and other elements not present
-in standard markdown.
-
-[pandoc]: http://johnmacfarlane.net/pandoc
-[pandoc User's Guide]: http://johnmacfarlane.net/pandoc/README.html
-[markdown]: http://daringfireball.net/projects/markdown
-[markdown "basics" page]: http://daringfireball.net/projects/markdown/basics
-[markdown syntax description]: http://daringfireball.net/projects/markdown/syntax 
-
-Markdown is pretty intuitive, since it is based on email conventions.
-Here are some examples to get you started:
-
-<table>
-<tr>
-<td>`*emphasized text*`</td>
-<td>*emphasized text*</td>
-</tr>
-<tr>
-<td>`**strong emphasis**`</td>
-<td>**strong emphasis**</td>
-</tr>
-<tr>
-<td>`` `literal text` ``</td>
-<td>`literal text`</td>
-</tr>
-<tr>
-<td>`\*escaped special characters\*`</td>
-<td>\*escaped special characters\*</td>
-</tr>
-<tr>
-<td>`[external link](http://google.com)`</td>
-<td>[external link](http://google.com)</td>
-</tr>
-<tr>
-<td>`![folder](/stylesheets/folder.png)`</td>
-<td>![folder](/stylesheets/folder.png)</td>
-</tr>
-<tr>
-<td>Wikilink: `[Front Page]()`</td>
-<td>Wikilink: [Front Page]()</td>
-</tr>
-<tr>
-<td>`H~2~O`</td>
-<td>H~2~O</td>
-</tr>
-<tr>
-<td>`10^100^`</td>
-<td>10^100^</td>
-</tr>
-<tr>
-<td>`~~strikeout~~`</td>
-<td>~~strikeout~~</td>
-</tr>
-<tr>
-<td>
-`$x = \frac{{ - b \pm \sqrt {b^2 - 4ac} }}{{2a}}$`
-</td>
-<td>
-$x = \frac{{ - b \pm \sqrt {b^2 - 4ac} }}{{2a}}$^[If this looks like
-code, it's because jsMath is
-not installed on your system.  Contact your administrator to request it.]
-</td>
-</tr>
-<tr>
-<td>
-`A simple footnote.^[Or is it so simple?]`
-</td>
-<td>
-A simple footnote.^[Or is it so simple?]
-</td>
-</tr>
-<tr>
-<td>
-<pre>
-> an indented paragraph,
-> usually used for quotations
-</pre>
-</td>
-<td>
-
-> an indented paragraph,
-> usually used for quotations
-
-</td>
-<tr>
-<td>
-<pre>
-    #!/bin/sh -e
-    # code, indented four spaces
-    echo "Hello world"
-</pre>
-</td>
-<td>
-
-    #!/bin/sh -e
-    # code, indented four spaces
-    echo "Hello world"
-
-</td>
-</tr>
-<tr>
-<td>
-<pre>
-* a bulleted list
-* second item
-    - sublist
-    - and more
-* back to main list
-    1. this item has an ordered
-    2. sublist
-        a) you can also use letters
-        b) another item
-</pre>
-</td>
-<td>
-
-* a bulleted list
-* second item
-    - sublist
-    - and more
-* back to main list
-    1. this item has an ordered
-    2. sublist
-        a) you can also use letters
-        b) another item
-
-</td>
-</tr>
-<tr>
-<td>
-<pre>
-Fruit        Quantity
---------  -----------
-apples         30,200
-oranges         1,998
-pears              42
-
-Table:  Our fruit inventory
-</pre>
-</td>
-<td>
-
-Fruit        Quantity
---------  -----------
-apples         30,200
-oranges         1,998
-pears              42
-
-Table:  Our fruit inventory
-
-</td>
-</tr>
-</table>
-
-For headings, prefix a line with one or more `#` signs:  one for a major heading,
-two for a subheading, three for a subsubheading.  Be sure to leave space before
-and after the heading.
-
-    # Markdown
-
-    Text...
- 
-    ## Some examples...
-   
-    Text...
-
-## Wiki links
-
-Links to other wiki pages are formed this way:  `[Page Name]()`.
-(Gitit converts markdown links with empty targets into wikilinks.)
-
-To link to a wiki page using something else as the link text:
-`[something else](Page Name)`.
-
-Note that page names may contain spaces and some special characters.
-They need not be CamelCase.  CamelCase words are *not* automatically
-converted to wiki links.
-
-Wiki pages may be organized into directories.  So, if you have
-several pages on wine, you may wish to organize them like so:
-
-    Wine/Pinot Noir
-    Wine/Burgundy
-    Wine/Cabernet Sauvignon
-
-Note that a wiki link `[Burgundy]()` that occurs inside the `Wine`
-directory will link to `Wine/Burgundy`, and not to `Burgundy`.
-To link to a top-level page called `Burgundy`, you'd have to use
-`[Burgundy](/Burgundy)`.
-
 # Creating and modifying pages
 
 ## Registering for an account
@@ -223,8 +27,8 @@
 
 ## Editing a page
 
-To edit a page, just double-click it, or click the "edit" button at
-the bottom right corner of the page.
+To edit a page, just click the "edit" button at the bottom right corner
+of the page.
 
 You can click "Preview" at any time to see how your changes will look.
 Nothing is saved until you press "Save."
@@ -232,6 +36,48 @@
 Note that you must provide a description of your changes.  This is to
 make it easier for others to see how a wiki page has been changed.
 
+## Page metadata
+
+Pages may optionally begin with a metadata block.  Here is an example:
+
+    ---
+    format: latex+lhs
+    categories: haskell math
+    toc: no
+    title: Haskell and
+      Category Theory
+    ...
+
+    \section{Why Category Theory?}
+
+The metadata block consists of a list of key-value pairs, each on a
+separate line. If needed, the value can be continued on one or more
+additional line, which must begin with a space. (This is illustrated by
+the "title" example above.) The metadata block must begin with a line
+`---` and end with a line `...` optionally followed by one or more blank
+lines.
+
+Currently the following keys are supported:
+
+format
+:   Overrides the default page type as specified in the configuration file.
+    Possible values are `markdown`, `rst`, `latex`, `html`, `markdown+lhs`,
+    `rst+lhs`, `latex+lhs`.  (Capitalization is ignored, so you can also
+    use `LaTeX`, `HTML`, etc.)  The `+lhs` variants indicate that the page
+    is to be interpreted as literate Haskell.  If this field is missing,
+    the default page type will be used.
+
+categories
+:   A space or comma separated list of categories to which the page belongs.
+
+toc
+:   Overrides default setting for table-of-contents in the configuration file.
+    Values can be `yes`, `no`, `true`, or `false` (capitalization is ignored).
+
+title
+:   By default the displayed page title is the page name.  This metadata element
+    overrides that default.
+
 ## Creating a new page
 
 To create a new page, just create a wiki link that links to it, and
@@ -271,9 +117,9 @@
 wiki, check the box "Overwrite existing file." Otherwise, leave it
 unchecked.
 
-To link to an uploaded file, just use its name in a regular markdown link.
+To link to an uploaded file, just use its name in a regular wiki link.
 For example, if you uploaded a picture `fido.jpg`, you can insert the
-picture into a page using the markdown:  `![fido](fido.jpg)`.
+picture into a (markdown-formatted) page as follows: `![fido](fido.jpg)`.
 If you uploaded a PDF `projection.pdf`, you can insert a link to it
 using:  `[projection](projection.pdf)`.
 
diff --git a/data/SampleConfig.hs b/data/SampleConfig.hs
deleted file mode 100644
--- a/data/SampleConfig.hs
+++ /dev/null
@@ -1,20 +0,0 @@
-Config {
-repository          = Git "wikidata",
-defaultPageType     = Markdown,
-userFile            = "gitit-users",
-templateFile        = "template.html",
-staticDir           = "static",
-tableOfContents     = False,
-maxUploadSize       = 100000,
-portNumber          = 5001,
-debugMode           = True,
-frontPage           = "Front Page",
-noEdit              = ["Help", "Front Page"],
-noDelete            = ["Help", "Front Page"],
-accessQuestion      = Just ("Enter the access code (to request an access code, contact me@somewhere.org):", ["abcd"]),
-useRecaptcha        = False,
-recaptchaPublicKey  = "",
-recaptchaPrivateKey = "",
-mimeTypesFile       = "/etc/mime.types"
-}
-
diff --git a/data/default.conf b/data/default.conf
new file mode 100644
--- /dev/null
+++ b/data/default.conf
@@ -0,0 +1,211 @@
+# gitit wiki configuration file
+
+port: 5001
+# sets the port on which the web server will run.
+
+repository-type: Git
+# specifies the type of repository used for wiki content.
+# Options are Git and Darcs.
+
+repository-path: wikidata
+# specifies the path of the repository directory.  If it does not
+# exist, gitit will create it on startup.
+
+authentication-method: form
+# 'form' means that users will be logged in and registered
+# using forms in the gitit web interface.  'http' means
+# that gitit will assume that HTTP authentication is in
+# place and take the logged in username from the "Authorization"
+# field of the HTTP request header (in addition,
+# the login/logout and registration links will be
+# suppressed).  'generic' means that gitit will assume that
+# some form of authentication is in place that directly
+# sets REMOTE_USER to the name of the authenticated user
+# (e.g. mod_auth_cas on apache).
+
+user-file: gitit-users
+# specifies the path of the file containing user login information.
+# If it does not exist, gitit will create it (with an empty user list).
+# This file is not used if 'http' is selected for authentication-method.
+
+static-dir: static
+# specifies the path of the static directory (containing javascript,
+# css, and images).  If it does not exist, gitit will create it
+# and populate it with required scripts, stylesheets, and images.
+
+default-page-type: Markdown
+# specifies the type of markup used to interpret pages in the wiki.
+# Possible values are Markdown, RST, LaTeX, HTML, Markdown+LHS, RST+LHS,
+# and LaTeX+LHS. (The +LHS variants treat the input as
+# literate Haskell. See pandoc's documentation for more details.) If
+# Markdown is selected, pandoc's syntax extensions (for footnotes,
+# delimited code blocks, etc.) will be enabled. Note that pandoc's
+# reStructuredText parser is not complete, so some pages may not be
+# rendered correctly if RST is selected. The same goes for LaTeX and
+# HTML.
+
+math: MathML
+# specifies how LaTeX math is to be displayed.  Possible values
+# are MathML, raw, and jsMath.  If mathml is selected, gitit will
+# convert LaTeX math to MathML and link in a script, MathMLinHTML.js,
+# that allows the MathML to be seen in Gecko browsers, IE +
+# mathplayer, and Opera.  In other browsers you may get a jumble
+# of characters. If raw is selected, the LaTeX math will be displayed
+# as raw LaTeX math. If jsMath is selected, gitit will link to
+# the script /js/jsMath/easy/load.js, and will assume that jsMath
+# has been installed into the js/jsMath directory. This is the most
+# portable solution.
+
+show-lhs-bird-tracks: no
+# specifies whether to show Haskell code blocks in "bird style",
+# with "> " at the beginning of each line.
+
+templates-dir: templates
+# specifies the path of the directory containing page templates.
+# If it does not exist, gitit will create it with default templates.
+# Users may wish to edit the templates to customize the appearance of
+# their wiki. The template files are HStringTemplate templates.
+# Variables to be interpolated appear between $'s. Literal $'s must be
+# backslash-escaped.
+
+log-file: gitit.log
+# specifies the path of gitit's log file.  If it does not exist,
+# gitit will create it. The log is in Apache combined log format.
+
+log-level: WARNING
+# determines how much information is logged.
+# Possible values (from most to least verbose) are DEBUG, INFO,
+# NOTICE, WARNING, ERROR, CRITICAL, ALERT, EMERGENCY.
+
+front-page: Front Page
+# specifies which wiki page is to be used as the wiki's front page.
+# Gitit creates a default front page on startup, if one does not exist
+# already.
+
+no-delete: Front Page, Help
+# specifies pages that cannot be deleted through the web interface.
+# (They can still be deleted directly using git or darcs.)
+# A comma-separated list of page names.  Leave blank to allow
+# every page to be deleted.
+
+no-edit: Help
+# specifies pages that cannot be edited through the web interface.
+# Leave blank to allow every page to be edited.
+
+default-summary:
+# specifies text to be used in the change description if the author
+# leaves the "description" field blank.  If default-summary is blank
+# (the default), the author will be required to fill in the description
+# field.
+
+table-of-contents: yes
+# specifies whether to print a tables of contents (with links to
+# sections) on each wiki page.
+
+plugins:
+# specifies a list of plugins to load.  Plugins may be specified
+# either by their path or by their module name.  If the plugin name
+# starts with Gitit.Plugin., gitit will assume that the plugin is
+# an installed module and will not try to find a source file.
+# Examples:
+# plugins: plugins/DotPlugin.hs, CapitalizeEmphasisPlugin.hs
+# plugins: plugins/DotPlugin
+# plugins: Gitit.Plugin.InterwikiLinks
+
+use-cache: no
+# specifies whether to cache rendered pages.  Note that if use-feed
+# is selected, feeds will be cached regardless of the value of use-cache.
+
+cache-dir: cache
+# directory where rendered pages will be cached
+
+max-upload-size: 100K
+# specifies an upper limit on the size (in bytes) of files uploaded
+# through the wiki's web interface.
+
+debug-mode: no
+# if "yes", causes debug information to be logged while gitit is running.
+
+compress-responses: yes
+# specifies whether HTTP responses should be compressed.
+
+mime-types-file: /etc/mime.types
+# specifies the path of a file containing mime type mappings.
+# Each line of the file should contain two fields, separated by
+# whitespace. The first field is the mime type, the second is a
+# file extension.  For example:
+# video/x-ms-wmx                    wmx
+# If the file is not found, some simple defaults will be used.
+
+use-recaptcha: no
+# if "yes", causes gitit to use the reCAPTCHA service
+# (http://recaptcha.net) to prevent bots from creating accounts.
+
+recaptcha-private-key:
+recaptcha-public-key:
+# specifies the public and private keys for the reCAPTCHA service.
+# To get these, you need to create an account at http://recaptcha.net.
+
+access-question:
+access-question-answers:
+# specifies a question that users must answer when they attempt to create
+# an account, along with a comma-separated list of acceptable answers.
+# This can be used to institute a rudimentary password for signing up as
+# a user on the wiki, or as an alternative to reCAPTCHA.
+# Example:
+# access-question:  What is the code given to you by Ms. X?
+# access-question-answers:  RED DOG, red dog
+
+mail-command: sendmail %s
+# specifies the command to use to send notification emails.
+# '%s' will be replaced by the destination email address.
+# The body of the message will be read from stdin.
+# If this field is left blank, password reset will not be offered.
+
+reset-password-message:
+  > From: nobody@$hostname$
+  > To: $useremail$
+  > Subject: Wiki password reset
+  >
+  > Dear $username$:
+  > 
+  > To reset your password, please follow the link below:
+  > http://$hostname$:$port$$resetlink$
+  > 
+  > Yours sincerely,
+  > The Wiki Master
+
+# gives the text of the message that will be sent to the user should she
+# want to reset her password, or change other registration info.
+# The lines must be indented, and must begin with '>'.  The initial
+# spaces and '> ' will be stripped off. $username$ will be replaced
+# by the user's username, $useremail$ by her email address,
+# $hostname$ by the hostname on which the wiki is running (as
+# returned by the hostname system call), $port$ by the port on
+# which the wiki is running, and $resetlink$ by the
+# relative path of a reset link derived from the user's existing
+# hashed password. If your gitit wiki is being proxied to a location
+# other than the root path of $port$, you should change the link to
+# reflect this: for example, to
+# http://$hostname$/path/to/wiki$resetlink$ or
+# http://gitit.$hostname$$resetlink$
+
+use-feed: no
+# specifies whether an ATOM feed should be enabled (for the site and for
+# individual pages)
+
+base-url:
+# the base URL of the wiki, to be used in constructing feed IDs.
+# If this field is left blank, gitit will get the base URL from the
+# request header 'Host'.  For most users, this should be fine, but
+# if you are proxying a gitit instance to a subdirectory URL, you will
+# want to set this manually.
+
+wiki-title: Wiki
+# the title of the wiki, used in feeds.
+
+feed-days: 14
+# number of days to be included in feeds.
+
+feed-refresh-time: 60
+# number of minutes to cache feeds before refreshing
diff --git a/data/markup.HTML b/data/markup.HTML
new file mode 100644
--- /dev/null
+++ b/data/markup.HTML
@@ -0,0 +1,32 @@
+# Markup
+
+The syntax for wiki pages is standard XHTML. All tags must be
+properly closed.
+
+## Wiki links
+
+Links to other wiki pages are formed this way:
+`<a href="">Page Name</a>`. (Gitit converts links with empty
+targets into wikilinks.)
+
+To link to a wiki page using something else as the link text:
+`<a href="Page+Name">something else</a>`.
+
+Note that page names may contain spaces and some special
+characters. They need not be CamelCase. CamelCase words are *not*
+automatically converted to wiki links.
+
+Wiki pages may be organized into directories. So, if you have
+several pages on wine, you may wish to organize them like so:
+
+    Wine/Pinot Noir
+    Wine/Burgundy
+    Wine/Cabernet Sauvignon
+
+Note that a wiki link `<a href="">Burgundy</a>` that occurs inside
+the `Wine` directory will link to `Wine/Burgundy`, and not to
+`Burgundy`. To link to a top-level page called `Burgundy`, you'd
+have to use `<a href="/Burgundy">Burgundy</a>`.
+
+To link to a directory listing for a subdirectory, use a trailing
+slash:  `<a href="">Wine/</a>` will link to a listing of the `Wine` subdirectory.
diff --git a/data/markup.LaTeX b/data/markup.LaTeX
new file mode 100644
--- /dev/null
+++ b/data/markup.LaTeX
@@ -0,0 +1,63 @@
+# Markup
+
+This wiki's pages are written in (a subset of) [LaTeX].
+
+  [LaTeX]: http://www.latex-project.org/
+
+The following commands and environments are recognized:
+
+-   `\emph{emphasis}`
+
+-   `\textbf{bold}`
+
+-   `verb!verbatim@@\#!`
+
+-   `\textsubscr{2}`
+
+-   `\sout{strikeout}`
+
+-   `\textsuperscript{2}`
+
+-   `$e = mc^2$`
+
+-   `$$e = mc^2$$`
+
+-   `\footnote{a footnote}`
+
+-   `\section{section}`, `\subsection{subsection}`, etc.
+
+-   `\begin{quote} . . . \end{quote}`
+
+-   `\begin{verbatim} . . . \end{verbatim}`
+
+-   `\begin{itemize} . . . \end{itemize}`
+
+-   `\begin{enumerate} . . . \end{enumerate}`
+
+## Wiki links
+
+Links to other wiki pages are formed this way: `\href{}{Page Name}`.
+(Gitit converts markdown links with empty targets into wikilinks.)
+
+To link to a wiki page using something else as the link text:
+`\href{Page Name}{Something else}`.
+
+Note that page names may contain spaces and some special
+characters. They need not be CamelCase. CamelCase words are *not*
+automatically converted to wiki links.
+
+Wiki pages may be organized into directories. So, if you have
+several pages on wine, you may wish to organize them like so:
+
+    Wine/Pinot Noir
+    Wine/Burgundy
+    Wine/Cabernet Sauvignon
+
+Note that a wiki link `\href{}{Burgundy}` that occurs inside the `Wine`
+directory will link to `Wine/Burgundy`, and not to `Burgundy`. To
+link to a top-level page called `Burgundy`, you'd have to use
+`\href{/Burgundy}{Burgundy}`.
+
+To link to a directory listing for a subdirectory, use a trailing
+slash:  `\href{}{Wine/}` will link to a listing of the `Wine` subdirectory.
+
diff --git a/data/markup.Markdown b/data/markup.Markdown
new file mode 100644
--- /dev/null
+++ b/data/markup.Markdown
@@ -0,0 +1,198 @@
+# Markdown
+
+This wiki's pages are written in [pandoc]'s extended form of [markdown].
+If you're not familiar with markdown, you should start by looking
+at the [markdown "basics" page] and the [markdown syntax description].
+Consult the [pandoc User's Guide] for information about pandoc's syntax
+for footnotes, tables, description lists, and other elements not present
+in standard markdown.
+
+[pandoc]: http://johnmacfarlane.net/pandoc
+[pandoc User's Guide]: http://johnmacfarlane.net/pandoc/README.html
+[markdown]: http://daringfireball.net/projects/markdown
+[markdown "basics" page]: http://daringfireball.net/projects/markdown/basics
+[markdown syntax description]: http://daringfireball.net/projects/markdown/syntax 
+
+Markdown is pretty intuitive, since it is based on email conventions.
+Here are some examples to get you started:
+
+<table>
+<tr>
+<td>`*emphasized text*`</td>
+<td>*emphasized text*</td>
+</tr>
+<tr>
+<td>`**strong emphasis**`</td>
+<td>**strong emphasis**</td>
+</tr>
+<tr>
+<td>`` `literal text` ``</td>
+<td>`literal text`</td>
+</tr>
+<tr>
+<td>`\*escaped special characters\*`</td>
+<td>\*escaped special characters\*</td>
+</tr>
+<tr>
+<td>`[external link](http://google.com)`</td>
+<td>[external link](http://google.com)</td>
+</tr>
+<tr>
+<td>`![folder](/img/icons/folder.png)`</td>
+<td>![folder](/img/icons/folder.png)</td>
+</tr>
+<tr>
+<td>Wikilink: `[Front Page]()`</td>
+<td>Wikilink: [Front Page]()</td>
+</tr>
+<tr>
+<td>`H~2~O`</td>
+<td>H~2~O</td>
+</tr>
+<tr>
+<td>`10^100^`</td>
+<td>10^100^</td>
+</tr>
+<tr>
+<td>`~~strikeout~~`</td>
+<td>~~strikeout~~</td>
+</tr>
+<tr>
+<td>
+`$x = \frac{{ - b \pm \sqrt {b^2 - 4ac} }}{{2a}}$`
+</td>
+<td>
+$x = \frac{{ - b \pm \sqrt {b^2 - 4ac} }}{{2a}}$^[If this looks like
+code, it's because jsMath is
+not installed on your system.  Contact your administrator to request it.]
+</td>
+</tr>
+<tr>
+<td>
+`A simple footnote.^[Or is it so simple?]`
+</td>
+<td>
+A simple footnote.^[Or is it so simple?]
+</td>
+</tr>
+<tr>
+<td>
+<pre>
+> an indented paragraph,
+> usually used for quotations
+</pre>
+</td>
+<td>
+
+> an indented paragraph,
+> usually used for quotations
+
+</td>
+<tr>
+<td>
+<pre>
+    #!/bin/sh -e
+    # code, indented four spaces
+    echo "Hello world"
+</pre>
+</td>
+<td>
+
+    #!/bin/sh -e
+    # code, indented four spaces
+    echo "Hello world"
+
+</td>
+</tr>
+<tr>
+<td>
+<pre>
+* a bulleted list
+* second item
+    - sublist
+    - and more
+* back to main list
+    1. this item has an ordered
+    2. sublist
+        a) you can also use letters
+        b) another item
+</pre>
+</td>
+<td>
+
+* a bulleted list
+* second item
+    - sublist
+    - and more
+* back to main list
+    1. this item has an ordered
+    2. sublist
+        a) you can also use letters
+        b) another item
+
+</td>
+</tr>
+<tr>
+<td>
+<pre>
+Fruit        Quantity
+--------  -----------
+apples         30,200
+oranges         1,998
+pears              42
+
+Table:  Our fruit inventory
+</pre>
+</td>
+<td>
+
+Fruit        Quantity
+--------  -----------
+apples         30,200
+oranges         1,998
+pears              42
+
+Table:  Our fruit inventory
+
+</td>
+</tr>
+</table>
+
+For headings, prefix a line with one or more `#` signs:  one for a major heading,
+two for a subheading, three for a subsubheading.  Be sure to leave space before
+and after the heading.
+
+    # Markdown
+
+    Text...
+ 
+    ## Some examples...
+   
+    Text...
+
+## Wiki links
+
+Links to other wiki pages are formed this way:  `[Page Name]()`.
+(Gitit converts markdown links with empty targets into wikilinks.)
+
+To link to a wiki page using something else as the link text:
+`[something else](Page Name)`.
+
+Note that page names may contain spaces and some special characters.
+They need not be CamelCase.  CamelCase words are *not* automatically
+converted to wiki links.
+
+Wiki pages may be organized into directories.  So, if you have
+several pages on wine, you may wish to organize them like so:
+
+    Wine/Pinot Noir
+    Wine/Burgundy
+    Wine/Cabernet Sauvignon
+
+Note that a wiki link `[Burgundy]()` that occurs inside the `Wine`
+directory will link to `Wine/Burgundy`, and not to `Burgundy`.
+To link to a top-level page called `Burgundy`, you'd have to use
+`[Burgundy](/Burgundy)`.
+
+To link to a directory listing for a subdirectory, use a trailing
+slash: `[Wine/]()` will link to a listing of the `Wine` subdirectory.
diff --git a/data/markup.RST b/data/markup.RST
new file mode 100644
--- /dev/null
+++ b/data/markup.RST
@@ -0,0 +1,42 @@
+# Markup
+
+This wiki's pages are written in [reStructuredText]. If you're
+not familiar with reStructuredText, you should start by looking at
+the [primer] and the [quick reference guide]. Note that not all
+reStructuredText constructs are currently supported.  Use the
+preview button if you're in doubt.
+
+  [reStructuredText]: http://docutils.sourceforge.net/rst.html
+  [primer]: http://docutils.sourceforge.net/docs/user/rst/quickstart.html
+  [quick reference guide]: http://docutils.sourceforge.net/docs/user/rst/quickref.html
+
+## Wiki links
+
+Links to other wiki pages are formed this way: `` `Page Name <>`_ ``.
+(Gitit converts markdown links with empty targets into wikilinks.)
+
+To link to a wiki page using something else as the link text:
+either `` `something else <Page+Name>`_ `` or
+
+    `something else`_
+
+    .. _`something else`: Page Name
+
+Note that page names may contain spaces and some special
+characters. They need not be CamelCase. CamelCase words are *not*
+automatically converted to wiki links.
+
+Wiki pages may be organized into directories. So, if you have
+several pages on wine, you may wish to organize them like so:
+
+    Wine/Pinot Noir
+    Wine/Burgundy
+    Wine/Cabernet Sauvignon
+
+Note that a wiki link `` `Burgundy <>`_ `` that occurs inside the `Wine`
+directory will link to `Wine/Burgundy`, and not to `Burgundy`. To
+link to a top-level page called `Burgundy`, you'd have to use
+`` `Burgundy </Burgundy>`_ ``.
+
+To link to a directory listing for a subdirectory, use a trailing
+slash:  `` `Wine/ <>`_ `` will link to a listing of the `Wine` subdirectory.
diff --git a/data/markupHelp/HTML b/data/markupHelp/HTML
new file mode 100644
--- /dev/null
+++ b/data/markupHelp/HTML
@@ -0,0 +1,50 @@
+~~~~~~~~
+<h1>Section heading</h1>
+<h2>Subsection</h2>
+<p>Formatting:
+<i>italics</i>,
+<b>bold</b>,
+super<sup>script</sup>,
+sub<sub>script</sub>,
+line<br/>break.
+</p>
+<blockquote>
+<p>Indented quotation</p>
+</blockquote>
+<p>Links:
+<a href="http://foo.bar">
+external</a>,
+<a href="">Wiki Link</a>,
+<img src="/img/banner.png"
+ alt="image"/>,
+</p>
+<p>Indented code block:</p>
+<pre><code>
+#include &lt;stdbool.h&gt;
+</code></pre>
+<ul>
+<li>bulleted</li>
+<li>list</li>
+</ul>
+<hr/>
+<ol>
+<li>ordered</li>
+<li>list
+<ol>
+<li>sublist</li>
+<li>another</li>
+</ol>
+</li>
+<li>item three</li>
+</ol>
+<dl>
+<dt>term</dt>
+<dd>definition</dd>
+<dt>orange</dt>
+<dd>orange fruit</dd>
+</dl>
+~~~~~~~~
+
+For more: [xhtml tutorial](http://www.w3schools.com/Xhtml/),
+[pandoc](http://johnmacfarlane.net/pandoc/README.html).
+
diff --git a/data/markupHelp/LaTeX b/data/markupHelp/LaTeX
new file mode 100644
--- /dev/null
+++ b/data/markupHelp/LaTeX
@@ -0,0 +1,54 @@
+~~~~~~~~
+\section{Section heading}
+
+\subsection{Subsection}
+
+Formatting: \emph{italics},
+\textbf{bold},
+super\textsuperscript{script},
+sub\textsubscr{script},
+\sout{strikeout}. A line break\\
+can be forced with \\ at
+the end of the line.
+
+\begin{quote}
+Indented quotation
+\end{quote}
+
+Links:
+\href{http://foo.bar}{external},
+\href{}{Wiki Link},
+\includegraphics{/img/banner.png},
+\href{#subsection}{to heading}.
+
+\begin{verbatim}
+#include <stdbool.h>
+\end{verbatim}
+
+\begin{itemize}
+\item bulleted
+\item list
+\end{itemize}
+
+\begin{enumerate}
+\item ordered
+\item list
+ \begin{enumerate}[a.]
+  \item sublist
+  \item another
+ \end{enumerate}
+\item item three
+\end{enumerate}
+
+\begin{description}
+\item[term] definition
+\item[orange] orange fruit
+\end{description}
+
+~~~~~~~~
+
+For more: [LaTeX], [pandoc].
+
+[LaTeX]: http://www.latex-project.org/
+[pandoc]: http://johnmacfarlane.net/pandoc/README.html
+
diff --git a/data/markupHelp/LaTeX+LHS b/data/markupHelp/LaTeX+LHS
new file mode 100644
--- /dev/null
+++ b/data/markupHelp/LaTeX+LHS
@@ -0,0 +1,60 @@
+~~~~~~~~
+\section{Section heading}
+
+\subsection{Subsection}
+
+Formatting: \emph{italics},
+\textbf{bold},
+super\textsuperscript{script},
+sub\textsubscr{script},
+\sout{strikeout}. A line break\\
+can be forced with \\ at
+the end of the line.
+
+\begin{quote}
+Indented quotation
+\end{quote}
+
+Links:
+\href{http://foo.bar}{external},
+\href{}{Wiki Link},
+\includegraphics{/img/banner.png},
+\href{#subsection}{to heading}.
+
+\begin{verbatim}
+#include <stdbool.h>
+\end{verbatim}
+
+Haskell code:
+\begin{code}
+fibs = 1 : 1 : zipWith (+)
+   fibs (tail fibs)
+\end{code}
+
+\begin{itemize}
+\item bulleted
+\item list
+\end{itemize}
+
+\begin{enumerate}
+\item ordered
+\item list
+ \begin{enumerate}[a.]
+  \item sublist
+  \item another
+ \end{enumerate}
+\item item three
+\end{enumerate}
+
+\begin{description}
+\item[term] definition
+\item[orange] orange fruit
+\end{description}
+
+~~~~~~~~
+
+For more: [LaTeX], [pandoc].
+
+[LaTeX]: http://www.latex-project.org/
+[pandoc]: http://johnmacfarlane.net/pandoc/README.html
+
diff --git a/data/markupHelp/Markdown b/data/markupHelp/Markdown
new file mode 100644
--- /dev/null
+++ b/data/markupHelp/Markdown
@@ -0,0 +1,51 @@
+~~~~~~~~
+# Section heading
+
+## Subsection
+
+Formatting:  *italics*,
+**bold**, super^script^,
+sub~script~, ~~strikeout~~.
+A line break  
+can be forced with two spaces
+at the end of the line.
+
+> Indented quotation
+
+Links:
+[external](http://google.com),
+[Wiki Link](),
+![image](/img/gitit-banner.png),
+[to heading](#section-heading).
+
+Indented code block:
+
+    #include <stdbool.h>
+
+Or use a delimited code block:
+
+~~~ { .haskell }
+let a = 1:a in head a
+~~~
+
+- bulleted
+- list
+
+* * * * *
+
+1.  ordered
+2.  list
+    a. sublist (indent 4 spaces)
+    b. another
+3.  item three
+
+term
+:   definition
+orange
+:   orange fruit
+
+~~~~~~~~
+
+For more: [markdown syntax](http://daringfireball.net/projects/markdown),
+[pandoc extensions](http://johnmacfarlane.net/pandoc/README.html).
+
diff --git a/data/markupHelp/Markdown+LHS b/data/markupHelp/Markdown+LHS
new file mode 100644
--- /dev/null
+++ b/data/markupHelp/Markdown+LHS
@@ -0,0 +1,59 @@
+~~~~~~~~
+# Section heading
+
+## Subsection
+
+Formatting:  *italics*,
+**bold**, super^script^,
+sub~script~, ~~strikeout~~.
+A line break  
+can be forced with two spaces
+at the end of the line.
+
+ > Indented quotation:
+ > note: the '>' must not
+ > be flush with the margin
+ > or what follows will be
+ > treated as Haskell code
+
+> -- bird-tracks Haskell:
+> fibs = 0 : 1 :
+>   zipWith (+) fibs (tail fibs)
+
+Links:
+[external](http://google.com),
+[Wiki Link](),
+![image](/img/gitit-banner.png),
+[to heading](#section-heading).
+
+Indented code block:
+
+    #include <stdbool.h>
+
+Or use a delimited code block:
+
+~~~ { .haskell }
+let a = 1:a in head a
+~~~
+
+- bulleted
+- list
+
+* * * * *
+
+1.  ordered
+2.  list
+    a. sublist (indent 4 spaces)
+    b. another
+3.  item three
+
+term
+:   definition
+orange
+:   orange fruit
+
+~~~~~~~~
+
+For more: [markdown syntax](http://daringfireball.net/projects/markdown),
+[pandoc extensions](http://johnmacfarlane.net/pandoc/README.html).
+
diff --git a/data/markupHelp/RST b/data/markupHelp/RST
new file mode 100644
--- /dev/null
+++ b/data/markupHelp/RST
@@ -0,0 +1,50 @@
+~~~~~~~~
+Section heading
+===============
+
+Subsection
+----------
+
+Formatting: *italics*,
+**bold**.
+
+    Indented quotation
+
+Links:
+`external <http://foo.org>`_,
+`Wiki Link <>`_, |image|,
+`heading <#subsection>`_.
+
+.. |image| image::
+     /img/gitit-banner.png
+
+::
+
+    let a = 1:a in head a
+
+-  bulleted
+-  list
+
+---------------
+
+1.  ordered
+2.  list
+   
+    a. sublist (indent 4 spaces)
+    b. another
+
+3.  item three
+
+term
+    definition
+orange
+    orange fruit
+
+~~~~~~~~
+
+For more: [reST primer],
+[quick reference guide].
+
+[reST primer]: http://docutils.sourceforge.net/docs/user/rst/quickstart.html
+[quick reference guide]: http://docutils.sourceforge.net/docs/user/rst/quickref.html
+
diff --git a/data/markupHelp/RST+LHS b/data/markupHelp/RST+LHS
new file mode 100644
--- /dev/null
+++ b/data/markupHelp/RST+LHS
@@ -0,0 +1,55 @@
+~~~~~~~~
+Section heading
+===============
+
+Subsection
+----------
+
+Formatting: *italics*,
+**bold**.
+
+    Indented quotation
+
+
+Links:
+`external <http://foo.org>`_,
+`Wiki Link <>`_, |image|,
+`heading <#subsection>`_.
+
+.. |image| image::
+     /img/gitit-banner.png
+
+::
+
+    let a = 1:a in head a
+
+> -- bird-style Haskell
+> fibs = 1 : 1 : zipWith (+)
+>   fibs (tail fibs)
+
+-  bulleted
+-  list
+
+--------------
+
+-  ordered
+-  list
+   
+   a. sublist (indent 4 spaces)
+   b. another
+
+-  item three
+
+term
+    definition
+orange
+    orange fruit
+
+~~~~~~~~
+
+For more: [reST primer],
+[quick reference guide].
+
+[reST primer]: http://docutils.sourceforge.net/docs/user/rst/quickstart.html
+[quick reference guide]: http://docutils.sourceforge.net/docs/user/rst/quickref.html
+
diff --git a/data/static/css/base-min.css b/data/static/css/base-min.css
new file mode 100644
--- /dev/null
+++ b/data/static/css/base-min.css
@@ -0,0 +1,7 @@
+/*
+Copyright (c) 2009, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.net/yui/license.txt
+version: 2.7.0
+*/
+body{margin:10px;}h1{font-size:138.5%;}h2{font-size:123.1%;}h3{font-size:108%;}h1,h2,h3{margin:1em 0;}h1,h2,h3,h4,h5,h6,strong,dt{font-weight:bold;}optgroup{font-weight:normal;}abbr,acronym{border-bottom:1px dotted #000;cursor:help;}em{font-style:italic;}del{text-decoration:line-through;}blockquote,ul,ol,dl{margin:1em;}ol,ul,dl{margin-left:2em;}ol li{list-style:decimal outside;}ul li{list-style:disc outside;}dl dd{margin-left:1em;}th,td{border:1px solid #000;padding:.5em;}th{font-weight:bold;text-align:center;}caption{margin-bottom:.5em;text-align:center;}sup{vertical-align:super;}sub{vertical-align:sub;}p,fieldset,table,pre{margin-bottom:1em;}button,input[type="checkbox"],input[type="radio"],input[type="reset"],input[type="submit"]{padding:1px;}
diff --git a/data/static/css/custom.css b/data/static/css/custom.css
new file mode 100644
--- /dev/null
+++ b/data/static/css/custom.css
@@ -0,0 +1,5 @@
+@import url("screen.css"); /* default gitit screen styles */
+@import url("hk-pyg.css"); /* for syntax highlighting */
+
+/* Put your custom style modifications here: */
+
diff --git a/data/static/css/hk-pyg.css b/data/static/css/hk-pyg.css
new file mode 100644
--- /dev/null
+++ b/data/static/css/hk-pyg.css
@@ -0,0 +1,20 @@
+/* Loosely based on pygment's default colors */
+table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode, table.sourceCode pre 
+   { margin: 0; padding: 0; border: 0; vertical-align: baseline; border: none; }
+td.lineNumbers { border-right: 1px solid #AAAAAA; text-align: right; color: #AAAAAA; padding-right: 5px; padding-left: 5px; } 
+td.sourceCode { padding-left: 5px; }
+pre.sourceCode { }
+pre.sourceCode span.Normal { }
+pre.sourceCode span.Keyword { color: #007020; font-weight: bold; } 
+pre.sourceCode span.DataType { color: #902000; }
+pre.sourceCode span.DecVal { color: #40a070; }
+pre.sourceCode span.BaseN { color: #40a070; }
+pre.sourceCode span.Float { color: #40a070; }
+pre.sourceCode span.Char { color: #4070a0; }
+pre.sourceCode span.String { color: #4070a0; }
+pre.sourceCode span.Comment { color: #60a0b0; font-style: italic; }
+pre.sourceCode span.Others { color: #007020; }
+pre.sourceCode span.Alert { color: red; font-weight: bold; }
+pre.sourceCode span.Function { color: #06287e; }
+pre.sourceCode span.RegionMarker { }
+pre.sourceCode span.Error { color: red; font-weight: bold; }
diff --git a/data/static/css/ie.css b/data/static/css/ie.css
new file mode 100644
--- /dev/null
+++ b/data/static/css/ie.css
@@ -0,0 +1,20 @@
+/* ie.css */
+body {text-align:center;}
+.container {text-align:left;}
+* html .column {overflow-x:hidden;}
+* html legend {margin:-18px -8px 16px 0;padding:0;}
+ol {margin-left:2em;}
+sup {vertical-align:text-top;}
+sub {vertical-align:text-bottom;}
+html>body p code {*white-space:normal;}
+hr {margin:-8px auto 11px;}
+.container ul { list-style: disc outside; margin-left: 2em; } /* IE can't handle :before and :after */
+.container ul li { text-indent: 0; margin-left: 0; }
+.container legend { margin-bottom: 1.6em; } /* IE form margin bug */
+sup, sub { font-size: 100%; } /* IE superscript & subscript bug */
+.container blockquote p, #content blockquote ul, #content blockquote ol, #content blockquote dl, #content blockquote pre, #content blockquote address, 
+.container blockquote table, #content blockquote form, #content blockquote h1, #content blockquote h2, #content blockquote h3, #content blockquote h4, #content blockquote h5, #content blockquote h6 { margin-top: .8em; margin-bottom: .8em; } /* IE can't handle :first-child */
+* html .container textarea, * html .container input { padding: 0; } /* IE < 7 form fix */
+.container input[type='submit'], .container input[type='button'] { padding: 0; } /* IE 7 button fix */
+.container legend+* { margin-top: 0; } /* we already added legend margin */
+a abbr, a acronym { text-decoration: underline; } /* IE 7 bug */
diff --git a/data/static/css/print.css b/data/static/css/print.css
new file mode 100644
--- /dev/null
+++ b/data/static/css/print.css
@@ -0,0 +1,50 @@
+body { 
+width:100% !important;
+margin:0 !important;
+padding:0 !important;
+line-height: 1.4;
+word-spacing:1.1pt;
+letter-spacing:0.2pt; font-family: "Times New Roman", serif; color: #000; background: none; font-size: 12pt; }
+
+/*Headings */
+h1,h2,h3,h4,h5,h6 { font-family: Helvetica, Arial, sans-serif; }
+h1{font-size:19pt;}
+h2{font-size:17pt;}
+h3{font-size:15pt;}
+h4,h5,h6{font-size:12pt;}
+
+h2.revision { font-size: 10pt; font-weight: normal; font-style: italic; text-align: right; }
+
+pre, code { font: 10pt Courier, monospace; } 
+blockquote { margin: 1.3em; padding: 1em;  font-size: 10pt; }
+hr { background-color: #ccc; }
+
+/* Images */
+img { float: left; margin: 1em 1.5em 1.5em 0; }
+a img { border: none; }
+
+/* Links */
+a:link, a:visited { background: transparent; font-weight: normal; text-decoration: underline; color:#333; }
+a:link[href^="http://"]:after, a[href^="http://"]:visited:after { content: " (" attr(href) ")"; font-size: 90%; }
+a[href^="http://"] {color:#000; }
+
+/* Table */
+table { margin: 1px; text-align:left; }
+th { font-weight: bold; }
+th,td { padding: 4px 10px 4px 0; }
+tfoot { font-style: italic; }
+caption { background: #fff; margin-bottom:2em; text-align:left; }
+thead {display: table-header-group;}
+tr {page-break-inside: avoid;} 
+
+/*hide various parts from the site*/
+
+#maincol { margin-left: 1em; margin-right: 1em; border: none; }
+#content { border: none; }
+#sidebar, #userbox, #footer {display:none;}
+#toc { display: none; }
+#categoryList { display: none; }
+h1 a:link, h2 a:link, h3 a:link, h4 a:link, h5 a:link, h6 a:link { text-decoration: none; }
+h1.pageTitle { font-size: 220%; }
+td.lineNumbers { display: none; }
+ul.tabs { display: none; }
diff --git a/data/static/css/reset-fonts-grids.css b/data/static/css/reset-fonts-grids.css
new file mode 100644
--- /dev/null
+++ b/data/static/css/reset-fonts-grids.css
@@ -0,0 +1,7 @@
+/*
+Copyright (c) 2009, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.net/yui/license.txt
+version: 2.7.0
+*/
+html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;}input,button,textarea,select{*font-size:100%;}body{font:13px/1.231 arial,helvetica,clean,sans-serif;*font-size:small;*font:x-small;}select,input,button,textarea,button{font:99% arial,helvetica,clean,sans-serif;}table{font-size:inherit;font:100%;}pre,code,kbd,samp,tt{font-family:monospace;*font-size:108%;line-height:100%;}body{text-align:center;}#doc,#doc2,#doc3,#doc4,.yui-t1,.yui-t2,.yui-t3,.yui-t4,.yui-t5,.yui-t6,.yui-t7{margin:auto;text-align:left;width:57.69em;*width:56.25em;}#doc2{width:73.076em;*width:71.25em;}#doc3{margin:auto 10px;width:auto;}#doc4{width:74.923em;*width:73.05em;}.yui-b{position:relative;}.yui-b{_position:static;}#yui-main .yui-b{position:static;}#yui-main,.yui-g .yui-u .yui-g{width:100%;}.yui-t1 #yui-main,.yui-t2 #yui-main,.yui-t3 #yui-main{float:right;margin-left:-25em;}.yui-t4 #yui-main,.yui-t5 #yui-main,.yui-t6 #yui-main{float:left;margin-right:-25em;}.yui-t1 .yui-b{float:left;width:12.30769em;*width:12.00em;}.yui-t1 #yui-main .yui-b{margin-left:13.30769em;*margin-left:13.05em;}.yui-t2 .yui-b{float:left;width:13.8461em;*width:13.50em;}.yui-t2 #yui-main .yui-b{margin-left:14.8461em;*margin-left:14.55em;}.yui-t3 .yui-b{float:left;width:23.0769em;*width:22.50em;}.yui-t3 #yui-main .yui-b{margin-left:24.0769em;*margin-left:23.62em;}.yui-t4 .yui-b{float:right;width:13.8456em;*width:13.50em;}.yui-t4 #yui-main .yui-b{margin-right:14.8456em;*margin-right:14.55em;}.yui-t5 .yui-b{float:right;width:18.4615em;*width:18.00em;}.yui-t5 #yui-main .yui-b{margin-right:19.4615em;*margin-right:19.125em;}.yui-t6 .yui-b{float:right;width:23.0769em;*width:22.50em;}.yui-t6 #yui-main .yui-b{margin-right:24.0769em;*margin-right:23.62em;}.yui-t7 #yui-main .yui-b{display:block;margin:0 0 1em 0;}#yui-main .yui-b{float:none;width:auto;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf,.yui-gc .yui-u,.yui-gd .yui-g,.yui-g .yui-gc .yui-u,.yui-ge .yui-u,.yui-ge .yui-g,.yui-gf .yui-g,.yui-gf .yui-u{float:right;}.yui-g div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first,.yui-ge div.first,.yui-gf div.first,.yui-g .yui-gc div.first,.yui-g .yui-ge div.first,.yui-gc div.first div.first{float:left;}.yui-g .yui-u,.yui-g .yui-g,.yui-g .yui-gb,.yui-g .yui-gc,.yui-g .yui-gd,.yui-g .yui-ge,.yui-g .yui-gf{width:49.1%;}.yui-gb .yui-u,.yui-g .yui-gb .yui-u,.yui-gb .yui-g,.yui-gb .yui-gb,.yui-gb .yui-gc,.yui-gb .yui-gd,.yui-gb .yui-ge,.yui-gb .yui-gf,.yui-gc .yui-u,.yui-gc .yui-g,.yui-gd .yui-u{width:32%;margin-left:1.99%;}.yui-gb .yui-u{*margin-left:1.9%;*width:31.9%;}.yui-gc div.first,.yui-gd .yui-u{width:66%;}.yui-gd div.first{width:32%;}.yui-ge div.first,.yui-gf .yui-u{width:74.2%;}.yui-ge .yui-u,.yui-gf div.first{width:24%;}.yui-g .yui-gb div.first,.yui-gb div.first,.yui-gc div.first,.yui-gd div.first{margin-left:0;}.yui-g .yui-g .yui-u,.yui-gb .yui-g .yui-u,.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u,.yui-ge .yui-g .yui-u,.yui-gf .yui-g .yui-u{width:49%;*width:48.1%;*margin-left:0;}.yui-g .yui-g .yui-u{width:48.1%;}.yui-g .yui-gb div.first,.yui-gb .yui-gb div.first{*margin-right:0;*width:32%;_width:31.7%;}.yui-g .yui-gc div.first,.yui-gd .yui-g{width:66%;}.yui-gb .yui-g div.first{*margin-right:4%;_margin-right:1.3%;}.yui-gb .yui-gc div.first,.yui-gb .yui-gd div.first{*margin-right:0;}.yui-gb .yui-gb .yui-u,.yui-gb .yui-gc .yui-u{*margin-left:1.8%;_margin-left:4%;}.yui-g .yui-gb .yui-u{_margin-left:1.0%;}.yui-gb .yui-gd .yui-u{*width:66%;_width:61.2%;}.yui-gb .yui-gd div.first{*width:31%;_width:29.5%;}.yui-g .yui-gc .yui-u,.yui-gb .yui-gc .yui-u{width:32%;_float:right;margin-right:0;_margin-left:0;}.yui-gb .yui-gc div.first{width:66%;*float:left;*margin-left:0;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf .yui-u{margin:0;}.yui-gb .yui-gb .yui-u{_margin-left:.7%;}.yui-gb .yui-g div.first,.yui-gb .yui-gb div.first{*margin-left:0;}.yui-gc .yui-g .yui-u,.yui-gd .yui-g .yui-u{*width:48.1%;*margin-left:0;}.yui-gb .yui-gd div.first{width:32%;}.yui-g .yui-gd div.first{_width:29.9%;}.yui-ge .yui-g{width:24%;}.yui-gf .yui-g{width:74.2%;}.yui-gb .yui-ge div.yui-u,.yui-gb .yui-gf div.yui-u{float:right;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf div.first{float:left;}.yui-gb .yui-ge .yui-u,.yui-gb .yui-gf div.first{*width:24%;_width:20%;}.yui-gb .yui-ge div.first,.yui-gb .yui-gf .yui-u{*width:73.5%;_width:65.5%;}.yui-ge div.first .yui-gd .yui-u{width:65%;}.yui-ge div.first .yui-gd div.first{width:32%;}#hd:after,#bd:after,#ft:after,.yui-g:after,.yui-gb:after,.yui-gc:after,.yui-gd:after,.yui-ge:after,.yui-gf:after{content:".";display:block;height:0;clear:both;visibility:hidden;}#hd,#bd,#ft,.yui-g,.yui-gb,.yui-gc,.yui-gd,.yui-ge,.yui-gf{zoom:1;}
diff --git a/data/static/css/screen.css b/data/static/css/screen.css
new file mode 100644
--- /dev/null
+++ b/data/static/css/screen.css
@@ -0,0 +1,121 @@
+@import url("reset-fonts-grids.css");
+@import url("base-min.css");
+
+html { background: #f9f9f9; color: black; } 
+
+fieldset { border: 1px solid #ccc; padding: 1em; }
+legend { font-weight: bold; margin-left: 1em; padding: 4px; }
+textarea, input[type='text'], input[type='password'], select { border: 1px solid #ccc; background: #fff; }
+textarea:hover, input[type='text']:hover, input[type='password']:hover, select:hover { border-color: #aaa; }
+textarea:focus, input[type='text']:focus, input[type='password']:focus, select:focus { border-color: #888; outline: 2px solid #ffffaa; }
+input, select { cursor: pointer; }
+input[type='text'] { cursor: text; }
+textarea,  input { padding: .3em .4em .15em .4em; }
+select { padding: .1em .2em 0 .2em; }
+option { padding: 0 .4em; }
+table, tr, td, th { border: none; }
+hr { height: 1px; color: #aaa; background-color: #aaa; border: 0; margin: .2em 0 .2em 0; }
+
+h1, h2, h3, h4, h5, h6 { font-weight: normal; border-bottom: 1px solid black; }
+
+h1.pageTitle { font-size: 197%; margin: 0.2em 0 .5em; }
+
+h1 { font-size: 153.9%; margin: 1.07em 0 .535em; }
+h2 { font-size: 138.5%; margin: 1.14em 0 .57em; }
+h3 { font-size: 123.1%; margin: 1.23em 0 .615em; }
+h4 { font-size: 116%; margin: 1.33em 0 .67em; }
+h5 { font-size: 108%; margin: 1.6em 0 .8em; }
+h6 { font-size: 100%; margin: 1.6em 0 .8em; }
+
+ul { list-style-type: square; }
+dt { font-weight: bold; margin-bottom: .1em; }
+
+blockquote { padding: 0 1.6em; color: #666; }
+
+a:link { text-decoration: underline; color: #36c; }
+a:visited { text-decoration: underline; color: #99c; }
+a:hover { text-decoration: underline; color: #c33; }
+a:active,  a:focus { text-decoration: underline; color: #000; }
+
+input.search_term { width: 95% }
+
+button:hover, a.button:hover { background-color:#dff4ff; border:1px solid #c2e1ef; color:#336699; }
+a.button:active, button:active { background-color:#6299c5; border:1px solid #6299c5; color:#fff; }
+
+h1 > a:link, h1 > a:active, h1 > a:hover, h1 > a:focus, h1 > a:visited,
+h2 > a:link, h2 > a:active, h2 > a:hover, h2 > a:focus, h2 > a:visited,
+h3 > a:link, h3 > a:active, h3 > a:hover, h3 > a:focus, h3 > a:visited,
+h4 > a:link, h4 > a:active, h4 > a:hover, h4 > a:focus, h4 > a:visited,
+h5 > a:link, h5 > a:active, h5 > a:hover, h5 > a:focus, h5 > a:visited,
+h6 > a:link, h6 > a:active, h6 > a:hover, h6 > a:focus, h6 > a:visited {
+        color: black; text-decoration: none; }
+
+#content { border: 1px solid #ccc; background-color: #fff; padding: 1em; font-size: 108%; }
+#content p, #content pre, #content li { line-height: 140%; }
+
+#userbox  { text-align: right; font-weight: bold; margin: 1em; }
+
+#logo { min-height: 50px; }
+
+#sidebar fieldset { background-color: white; margin-bottom: 1em; padding: 0; font-size: 93%; }
+#sidebar fieldset, #sidebar fieldset legend { font-weight: normal; }
+#sidebar ul { padding: 0; margin: 0; margin-left: 1.6em; line-height: 1.5em; }
+#sidebar ul li { color: #888; list-style: square; }
+
+div#toc { background-color: #f9f9f9; border: 10px solid white; margin: 0.8em; margin-right: 0; padding: 0.4em; }
+#toc ul { padding: 0 0 0 1em; margin: 0; list-style: none; }
+
+#sidebar input, #sidebar select { font-size:  93%; padding: 0.1em; }
+#sidebar input[type='submit'] { border: none; background-color: #ccc; color: white; }
+
+#exportbox select { width: 8.5em; border: 1px solid #ccc; padding: 0; }
+#exportbox { margin: 0.3em 0 0.5em 0.4em; padding: 0; }
+
+#footer { padding: 1em; color: #888; text-align: center; font-size: 93%; }
+
+#searchform { padding: 0; margin: 0.3em 0 0.5em 0.4em; }
+#searchform input[type='text'] { width: 8.5em; border: 1px solid #ccc; }
+
+div#categoryList { padding: 0em; margin: 1em 0 0 0; border: 1px dashed #ccc; }
+#categoryList ul > li { display: inline; padding-right: 1em; }
+
+#editform textarea { height: 25em; width: 98%; font-family: monospace; font-size: 93%; }
+#editform #logMsg { width: 98%; margin-right: 1em; margin-bottom: 0.3em; }
+
+#goform { padding: 0; margin: 0.3em 0 0.5em 0.4em; }
+#goform input[type='text'] { width: 8.5em; border: 1px solid #ccc; }
+
+.search_result { margin-bottom: 15px; }
+.search_result .match { margin-bottom: 15px; }
+
+code { font-size: 93%; }
+pre.matches { margin: 0; padding: 0; }
+
+.added { background-color: yellow; }
+.deleted { text-decoration: line-through; color: gray; }
+
+/* .req is used to hide a honeypot in a form */
+.req { display: none; }
+
+ul.messages > li { color: red; list-style: square; font-weight: bold; }
+
+ul.tabs { padding: 0; margin: 0 0 1px 0; }
+ul.tabs li { display: inline; border: 1px solid #ccc; border-bottom: none; padding: 0 0.6em 0 0.6em;
+    margin: 0 0 0 1.2em; background: white; }
+ul.tabs li.selected { border-bottom: 3px solid white; }
+ul.tabs li a { text-decoration: none; font-size: 93%; font-weight: bold; margin: 0; color: #36c; }
+
+.index ul { list-style: none; margin: 0; padding: 0; }
+.index li { list-style: none; background-position: 0 1px; background-repeat: no-repeat; padding-left: 20px; }
+.index li.page { background-image: url(../img/icons/page.png); }
+.index li.folder { background-image: url(../img/icons/folder.png); }
+.index a { color: #000000; cursor: pointer; text-decoration: none; }
+.index a:hover { text-decoration: underline; }
+
+a.updir { font-weight: bold; }
+
+h2.revision { font-size: 100%; color: #888; font-style: italic; border: none; margin: 0 0 0.5em 0; padding: 0; }
+
+div.markupHelp pre { font-size: 77%; overflow: auto; }
+
+.login { display: none; }
diff --git a/data/static/img/gitit-dog.png b/data/static/img/gitit-dog.png
new file mode 100644
Binary files /dev/null and b/data/static/img/gitit-dog.png differ
diff --git a/data/static/img/icons/folder.png b/data/static/img/icons/folder.png
new file mode 100644
Binary files /dev/null and b/data/static/img/icons/folder.png differ
diff --git a/data/static/img/icons/page.png b/data/static/img/icons/page.png
new file mode 100644
Binary files /dev/null and b/data/static/img/icons/page.png differ
diff --git a/data/static/js/MathMLinHTML.js b/data/static/js/MathMLinHTML.js
new file mode 100644
--- /dev/null
+++ b/data/static/js/MathMLinHTML.js
@@ -0,0 +1,76 @@
+/* 
+March 19, 2004 MathHTML (c) Peter Jipsen http://www.chapman.edu/~jipsen
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or (at
+your option) any later version.
+This program is distributed in the hope that it will be useful, but 
+WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
+or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License 
+(at http://www.gnu.org/copyleft/gpl.html) for more details.
+*/
+
+function convertMath(node) {// for Gecko
+  if (node.nodeType==1) {
+    var newnode = 
+      document.createElementNS("http://www.w3.org/1998/Math/MathML",
+        node.nodeName.toLowerCase());
+    for(var i=0; i < node.attributes.length; i++)
+      newnode.setAttribute(node.attributes[i].nodeName,
+        node.attributes[i].nodeValue);
+    for (var i=0; i<node.childNodes.length; i++) {
+      var st = node.childNodes[i].nodeValue;
+      if (st==null || st.slice(0,1)!=" " && st.slice(0,1)!="\n") 
+        newnode.appendChild(convertMath(node.childNodes[i]));
+    }
+    return newnode;
+  }
+  else return node;
+}
+
+function convert() {
+  var mmlnode = document.getElementsByTagName("math");
+  var st,str,node,newnode;
+  for (var i=0; i<mmlnode.length; i++)
+    if (document.createElementNS!=null)
+      mmlnode[i].parentNode.replaceChild(convertMath(mmlnode[i]),mmlnode[i]);
+    else { // convert for IE
+      str = "";
+      node = mmlnode[i];
+      while (node.nodeName!="/MATH") {
+        st = node.nodeName.toLowerCase();
+        if (st=="#text") str += node.nodeValue;
+        else {
+          str += (st.slice(0,1)=="/" ? "</m:"+st.slice(1) : "<m:"+st);
+          if (st.slice(0,1)!="/") 
+             for(var j=0; j < node.attributes.length; j++)
+               if (node.attributes[j].nodeValue!="italic" &&
+                 node.attributes[j].nodeValue!="" &&
+                 node.attributes[j].nodeValue!="inherit" &&
+                 node.attributes[j].nodeValue!=undefined)
+                 str += " "+node.attributes[j].nodeName+"="+
+                     "\""+node.attributes[j].nodeValue+"\"";
+          str += ">";
+        }
+        node = node.nextSibling;
+        node.parentNode.removeChild(node.previousSibling);
+      }
+      str += "</m:math>";
+      newnode = document.createElement("span");
+      node.parentNode.replaceChild(newnode,node);
+      newnode.innerHTML = str;
+    }
+}
+
+if (document.createElementNS==null) {
+  document.write("<object id=\"mathplayer\"\
+  classid=\"clsid:32F66A20-7614-11D4-BD11-00104BD3F987\"></object>");
+  document.write("<?import namespace=\"m\" implementation=\"#mathplayer\"?>");
+}
+if(typeof window.addEventListener != 'undefined'){
+  window.addEventListener('load', convert, false);
+}
+if(typeof window.attachEvent != 'undefined') {
+  window.attachEvent('onload', convert);
+}
diff --git a/data/static/js/dragdiff.js b/data/static/js/dragdiff.js
new file mode 100644
--- /dev/null
+++ b/data/static/js/dragdiff.js
@@ -0,0 +1,21 @@
+$(document).ready(function(){
+    $("#content").prepend("<p>Drag one revision onto another to see differences.</p>");
+    $(".difflink").draggable({helper: "clone"}); 
+    $(".difflink").droppable({
+         accept: ".difflink",
+         drop: function(ev, ui) {
+            var targetOrder = parseInt($(this).attr("order"));
+            var sourceOrder = parseInt($(ui.draggable).attr("order"));
+            var diffurl = $(this).attr("diffurl");
+            if (targetOrder < sourceOrder) {
+                var fromRev = $(this).attr("revision");
+                var toRev   = $(ui.draggable).attr("revision");
+            } else {
+                var toRev   = $(this).attr("revision");
+                var fromRev = $(ui.draggable).attr("revision");
+            };
+            location.href = diffurl + '?from=' + fromRev + '&to=' + toRev;
+            }
+        });
+});
+
diff --git a/data/static/js/jquery-ui.packed.js b/data/static/js/jquery-ui.packed.js
new file mode 100644
--- /dev/null
+++ b/data/static/js/jquery-ui.packed.js
@@ -0,0 +1,1 @@
+eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('(6(D){b A=D.4e.1B;D.4e.1B=6(){D("*",4).1j(4).21("1B");d A.2W(4,4g)};6 C(E){6 G(H){b I=H.24;d(I.2G!="3I"&&I.6g!="3F")}b F=G(E);(F&&D.1c(D.6m(E,"1X"),6(){d(F=G(4))}));d F}D.1x(D.6e[":"],{p:6(F,G,E){d D.p(F,E[3])},6v:6(F,G,E){b H=F.62.61();d(F.64>=0&&(("a"==H&&F.1q)||(/5A|2L|67|5B/.18(H)&&"3F"!=F.4R&&!F.v))&&C(F))}});D.6U={6w:8,6S:20,6X:6Z,6C:17,6D:46,6x:40,75:35,6Y:13,6V:27,76:36,6t:45,68:37,69:6c,65:6b,6s:6i,6r:6y,5Z:60,6o:6u,6n:34,6l:33,6q:6k,6f:39,6h:16,63:32,5Y:9,6d:38};6 B(I,E,J,H){6 G(L){b K=D[I][E][L]||[];d(2T K=="2S"?K.4f(/,?\\s+/):K)}b F=G("5m");5(H.X==1&&2T H[0]=="2S"){F=F.5l(G("5P"))}d(D.3q(J,F)!=-1)}D.2g=6(E,F){b G=E.4f(".")[0];E=E.4f(".")[1];D.4e[E]=6(K){b I=(2T K=="2S"),J=5f.2y.6a.12(4g,1);5(I&&K.6W(0,1)=="4Q"){d 4}5(I&&B(G,E,K,J)){b H=D.p(4[0],E);d(H?H[K].2W(H,J):1Q)}d 4.1c(6(){b L=D.p(4,E);(!L&&!I&&D.p(4,E,6T D[G][E](4,K)));(L&&I&&D.3s(L[K])&&L[K].2W(L,J))})};D[G][E]=6(I,J){b H=4;4.2v=E;4.4N=D[G][E].6Q||E;4.5J=G+"-"+E;4.7=D.1x({},D.2g.2B,D[G][E].2B,D.5T&&D.5T.4a(I)[E],J);4.k=D(I).1A("74."+E,6(M,K,L){d H.2j(K,L)}).1A("71."+E,6(L,K){d H.4k(K)}).1A("1B",6(){d H.2f()});4.3f()};D[G][E].2y=D.1x({},D.2g.2y,F);D[G][E].5P="5O"};D.2g.2y={3f:6(){},2f:6(){4.k.2E(4.2v)},5O:6(G,H){b F=G,E=4;5(2T G=="2S"){5(H===1Q){d 4.4k(G)}F={};F[G]=H}D.1c(F,6(I,J){E.2j(I,J)})},4k:6(E){d 4.7[E]},2j:6(E,F){4.7[E]=F;5(E=="v"){4.k[F?"Z":"1f"](4.5J+"-v")}},4j:6(){4.2j("v",o)},4m:6(){4.2j("v",Y)},2b:6(F,H,G){b E=(F==4.4N?F:4.4N+F);H=H||D.2h.6E({4R:E,30:4.k[0]});d 4.k.21(E,[H,G],4.7[F])}};D.2g.2B={v:o};D.c={1k:{1j:6(F,E,I){b H=D.c[F].2y;2A(b G 6z I){H.2Y[G]=H.2Y[G]||[];H.2Y[G].2U([E,I[G]])}},12:6(E,G,F){b I=E.2Y[G];5(!I){d}2A(b H=0;H<I.X;H++){5(E.7[I[H][0]]){I[H][1].2W(E.k,F)}}}},3e:{},n:6(E){5(D.c.3e[E]){d D.c.3e[E]}b F=D(\'<2V 56="c-6F">\').Z(E).n({w:"1p",j:"-5W",h:"-5W",2G:"44"}).1Y("1y");D.c.3e[E]=!!((!(/2i|3r/).18(F.n("2D"))||(/^[1-9]/).18(F.n("19"))||(/^[1-9]/).18(F.n("1d"))||!(/3I/).18(F.n("6K"))||!(/6H|6I\\(0, 0, 0, 0\\)/).18(F.n("6R"))));6J{D("1y").4a(0).52(F.4a(0))}6L(G){}d D.c.3e[E]},6N:6(E){d D(E).1J("3c","5k").n("5j","3I").1A("5h.c",6(){d o})},6M:6(E){d D(E).1J("3c","6G").n("5j","").1D("5h.c")},6A:6(H,E){5(D(H).n("1F")=="3F"){d o}b G=(E&&E=="h")?"1n":"1m",F=o;5(H[G]>0){d Y}H[G]=1;F=(H[G]>0);H[G]=0;d F}};D.c.4q={5r:6(){b E=4;4.k.1A("6B."+4.2v,6(F){d E.53(F)});5(D.1I.2k){4.4Z=4.k.1J("3c");4.k.1J("3c","5k")}4.6P=o},5y:6(){4.k.1D("."+4.2v);(D.1I.2k&&4.k.1J("3c",4.4Z))},53:6(G){(4.28&&4.3i(G));4.3J=G;b E=4,H=(G.70==1),F=(2T 4.7.3S=="2S"?D(G.30).3X().1j(G.30).1S(4.7.3S).X:o);5(!H||F||!4.3G(G)){d Y}4.3K=!4.7.3x;5(!4.3K){4.72=3U(6(){E.3K=Y},4.7.3x)}5(4.4B(G)&&4.4A(G)){4.28=(4.3j(G)!==o);5(!4.28){G.73();d Y}}4.4K=6(I){d E.55(I)};4.4I=6(I){d E.3i(I)};D(u).1A("5F."+4.2v,4.4K).1A("5w."+4.2v,4.4I);d o},55:6(E){5(D.1I.2k&&!E.5B){d 4.3i(E)}5(4.28){4.2X(E);d o}5(4.4B(E)&&4.4A(E)){4.28=(4.3j(4.3J,E)!==o);(4.28?4.2X(E):4.3i(E))}d!4.28},3i:6(E){D(u).1D("5F."+4.2v,4.4K).1D("5w."+4.2v,4.4I);5(4.28){4.28=o;4.3p(E)}d o},4B:6(E){d(1o.4y(1o.1R(4.3J.2u-E.2u),1o.1R(4.3J.2m-E.2m))>=4.7.4G)},4A:6(E){d 4.3K},3j:6(E){},2X:6(E){},3p:6(E){},3G:6(E){d Y}};D.c.4q.2B={3S:15,4G:1,3x:0}})(3b);(6(A){A.2g("c.z",A.1x({},A.c.4q,{5H:6(C){b B=!4.7.2H||!A(4.7.2H,4.k).X?Y:o;A(4.7.2H,4.k).2Z("*").5K().1c(6(){5(4==C.30){B=Y}});d B},5a:6(){b C=4.7;b B=A.3s(C.t)?A(C.t.2W(4.k[0],[e])):(C.t=="4l"?4.k.4l():4.k);5(!B.3X("1y").X){B.1Y((C.1Y=="V"?4.k[0].1X:C.1Y))}5(B[0]!=4.k[0]&&!(/(26|1p)/).18(B.n("w"))){B.n("w","1p")}d B},3f:6(){5(4.7.t=="4C"&&!(/^(?:r|a|f)/).18(4.k.n("w"))){4.k[0].24.w="11"}(4.7.2O&&4.k.Z(4.7.2O+"-z"));(4.7.v&&4.k.Z("c-z-v"));4.5r()},3G:6(B){b C=4.7;5(4.t||C.v||A(B.30).3W(".c-6j-2H")){d o}4.2H=4.5H(B);5(!4.2H){d o}d Y},3j:6(D){b E=4.7;4.t=4.5a();5(A.c.14){A.c.14.2C=4}4.1w={h:(1i(4.k.n("66"),10)||0),j:(1i(4.k.n("6O"),10)||0)};4.1C=4.t.n("w");4.m=4.k.m();4.m={j:4.m.j-4.1w.j,h:4.m.h-4.1w.h};4.m.1a={h:D.2u-4.m.h,j:D.2m-4.m.j};4.5e();4.1U=4.t.1U();b B=4.1U.m();5(4.1U[0]==u.1y&&A.1I.7q){B={j:0,h:0}}4.m.V={j:B.j+(1i(4.1U.n("4z"),10)||0),h:B.h+(1i(4.1U.n("4x"),10)||0)};5(4.1C=="11"){b C=4.k.w();4.m.11={j:C.j-(1i(4.t.n("j"),10)||0)+4.2p.1m(),h:C.h-(1i(4.t.n("h"),10)||0)+4.2n.1n()}}1e{4.m.11={j:0,h:0}}4.2w=4.4J(D);4.4w();5(E.5d){4.57(E.5d)}A.1x(4,{4v:(4.1C=="1p"&&(!4.2p[0].1V||(/(2o|1y)/i).18(4.2p[0].1V))),4r:(4.1C=="1p"&&(!4.2n[0].1V||(/(2o|1y)/i).18(4.2n[0].1V))),4u:4.2p[0]!=4.1U[0]&&!(4.2p[0]==u&&(/(1y|2o)/i).18(4.1U[0].1V)),4s:4.2n[0]!=4.1U[0]&&!(4.2n[0]==u&&(/(1y|2o)/i).18(4.1U[0].1V))});5(E.U){4.5s()}4.23("22",D);4.4w();5(A.c.14&&!E.5E){A.c.14.3P(4,D)}4.t.Z("c-z-3E");4.2X(D);d Y},5e:6(){4.2p=6(B){3A{5(/2i|2c/.18(B.n("1F"))||(/2i|2c/).18(B.n("1F-y"))){d B}B=B.V()}3w(B[0].1X);d A(u)}(4.t);4.2n=6(B){3A{5(/2i|2c/.18(B.n("1F"))||(/2i|2c/).18(B.n("1F-x"))){d B}B=B.V()}3w(B[0].1X);d A(u)}(4.t)},57:6(B){5(B.h!=1Q){4.m.1a.h=B.h+4.1w.h}5(B.5n!=1Q){4.m.1a.h=4.1h.1d-B.5n+4.1w.h}5(B.j!=1Q){4.m.1a.j=B.j+4.1w.j}5(B.5q!=1Q){4.m.1a.j=4.1h.19-B.5q+4.1w.j}},4w:6(){4.1h={1d:4.t.5b(),19:4.t.5c()}},5s:6(){b E=4.7;5(E.U=="V"){E.U=4.t[0].1X}5(E.U=="u"||E.U=="2x"){4.U=[0-4.m.11.h-4.m.V.h,0-4.m.11.j-4.m.V.j,A(E.U=="u"?u:2x).1d()-4.m.11.h-4.m.V.h-4.1h.1d-4.1w.h-(1i(4.k.n("5t"),10)||0),(A(E.U=="u"?u:2x).19()||u.1y.1X.5u)-4.m.11.j-4.m.V.j-4.1h.19-4.1w.j-(1i(4.k.n("5v"),10)||0)]}5(!(/^(u|2x|V)$/).18(E.U)){b C=A(E.U)[0];b D=A(E.U).m();b B=(A(C).n("1F")!="3F");4.U=[D.h+(1i(A(C).n("4x"),10)||0)-4.m.11.h-4.m.V.h,D.j+(1i(A(C).n("4z"),10)||0)-4.m.11.j-4.m.V.j,D.h+(B?1o.4y(C.82,C.2K):C.2K)-(1i(A(C).n("4x"),10)||0)-4.m.11.h-4.m.V.h-4.1h.1d-4.1w.h-(1i(4.k.n("5t"),10)||0),D.j+(B?1o.4y(C.5u,C.2N):C.2N)-(1i(A(C).n("4z"),10)||0)-4.m.11.j-4.m.V.j-4.1h.19-4.1w.j-(1i(4.k.n("5v"),10)||0)]}},1L:6(C,D){5(!D){D=4.w}b B=C=="1p"?1:-1;d{j:(D.j+4.m.11.j*B+4.m.V.j*B-(4.1C=="26"||4.4v||4.4u?0:4.2p.1m())*B+(4.1C=="26"?A(u).1m():0)*B+4.1w.j*B),h:(D.h+4.m.11.h*B+4.m.V.h*B-(4.1C=="26"||4.4r||4.4s?0:4.2n.1n())*B+(4.1C=="26"?A(u).1n():0)*B+4.1w.h*B)}},4J:6(E){b F=4.7;b B={j:(E.2m-4.m.1a.j-4.m.11.j-4.m.V.j+(4.1C=="26"||4.4v||4.4u?0:4.2p.1m())-(4.1C=="26"?A(u).1m():0)),h:(E.2u-4.m.1a.h-4.m.11.h-4.m.V.h+(4.1C=="26"||4.4r||4.4s?0:4.2n.1n())-(4.1C=="26"?A(u).1n():0))};5(!4.2w){d B}5(4.U){5(B.h<4.U[0]){B.h=4.U[0]}5(B.j<4.U[1]){B.j=4.U[1]}5(B.h>4.U[2]){B.h=4.U[2]}5(B.j>4.U[3]){B.j=4.U[3]}}5(F.1Z){b D=4.2w.j+1o.5o((B.j-4.2w.j)/F.1Z[1])*F.1Z[1];B.j=4.U?(!(D<4.U[1]||D>4.U[3])?D:(!(D<4.U[1])?D-F.1Z[1]:D+F.1Z[1])):D;b C=4.2w.h+1o.5o((B.h-4.2w.h)/F.1Z[0])*F.1Z[0];B.h=4.U?(!(C<4.U[0]||C>4.U[2])?C:(!(C<4.U[0])?C-F.1Z[0]:C+F.1Z[0])):C}d B},2X:6(B){4.w=4.4J(B);4.1s=4.1L("1p");4.w=4.23("2a",B)||4.w;5(!4.7.3g||4.7.3g!="y"){4.t[0].24.h=4.w.h+"3v"}5(!4.7.3g||4.7.3g!="x"){4.t[0].24.j=4.w.j+"3v"}5(A.c.14){A.c.14.2a(4,B)}d o},3p:6(C){b D=o;5(A.c.14&&!4.7.5E){b D=A.c.14.2F(4,C)}5((4.7.2q=="81"&&!D)||(4.7.2q=="80"&&D)||4.7.2q===Y||(A.3s(4.7.2q)&&4.7.2q.12(4.k,D))){b B=4;A(4.t).4U(4.2w,1i(4.7.77,10)||5z,6(){B.23("1W",C);B.4L()})}1e{4.23("1W",C);4.4L()}d o},4L:6(){4.t.1f("c-z-3E");5(4.7.t!="4C"&&!4.3m){4.t.1B()}4.t=15;4.3m=o},2Y:{},3k:6(B){d{t:4.t,w:4.w,3T:4.1s,7:4.7}},23:6(C,B){A.c.1k.12(4,C,[B,4.3k()]);5(C=="2a"){4.1s=4.1L("1p")}d 4.k.21(C=="2a"?C:"2a"+C,[B,4.3k()],4.7[C])},2f:6(){5(!4.k.p("z")){d}4.k.2E("z").1D(".z").1f("c-z c-z-3E c-z-v");4.5y()}}));A.1x(A.c.z,{2B:{1Y:"V",3g:o,3S:":5A",3x:0,4G:1,t:"4C",2d:"3r",2O:"c"}});A.c.1k.1j("z","2D",{22:6(D,C){b B=A("1y");5(B.n("2D")){C.7.4E=B.n("2D")}B.n("2D",C.7.2D)},1W:6(C,B){5(B.7.4E){A("1y").n("2D",B.7.4E)}}});A.c.1k.1j("z","1K",{22:6(D,C){b B=A(C.t);5(B.n("1K")){C.7.4F=B.n("1K")}B.n("1K",C.7.1K)},1W:6(C,B){5(B.7.4F){A(B.t).n("1K",B.7.4F)}}});A.c.1k.1j("z","1O",{22:6(D,C){b B=A(C.t);5(B.n("1O")){C.7.4M=B.n("1O")}B.n("1O",C.7.1O)},1W:6(C,B){5(B.7.4M){A(B.t).n("1O",B.7.4M)}}});A.c.1k.1j("z","3d",{22:6(C,B){A(B.7.3d===Y?"7V":B.7.3d).1c(6(){A(\'<2V 56="c-z-3d" 24="7U: #7T;"></2V>\').n({1d:4.2K+"3v",19:4.2N+"3v",w:"1p",1O:"0.7W",1K:7X}).n(A(4).m()).1Y("1y")})},1W:6(C,B){A("2V.c-z-3d").1c(6(){4.1X.52(4)})}});A.c.1k.1j("z","2c",{22:6(D,C){b E=C.7;b B=A(4).p("z");E.1N=E.1N||20;E.1P=E.1P||20;B.1H=6(F){3A{5(/2i|2c/.18(F.n("1F"))||(/2i|2c/).18(F.n("1F-y"))){d F}F=F.V()}3w(F[0].1X);d A(u)}(4);B.1M=6(F){3A{5(/2i|2c/.18(F.n("1F"))||(/2i|2c/).18(F.n("1F-x"))){d F}F=F.V()}3w(F[0].1X);d A(u)}(4);5(B.1H[0]!=u&&B.1H[0].1V!="3y"){B.4c=B.1H.m()}5(B.1M[0]!=u&&B.1M[0].1V!="3y"){B.4b=B.1M.m()}},2a:6(E,D){b F=D.7,C=o;b B=A(4).p("z");5(B.1H[0]!=u&&B.1H[0].1V!="3y"){5((B.4c.j+B.1H[0].2N)-E.2m<F.1N){B.1H[0].1m=C=B.1H[0].1m+F.1P}5(E.2m-B.4c.j<F.1N){B.1H[0].1m=C=B.1H[0].1m-F.1P}}1e{5(E.2m-A(u).1m()<F.1N){C=A(u).1m(A(u).1m()-F.1P)}5(A(2x).19()-(E.2m-A(u).1m())<F.1N){C=A(u).1m(A(u).1m()+F.1P)}}5(B.1M[0]!=u&&B.1M[0].1V!="3y"){5((B.4b.h+B.1M[0].2K)-E.2u<F.1N){B.1M[0].1n=C=B.1M[0].1n+F.1P}5(E.2u-B.4b.h<F.1N){B.1M[0].1n=C=B.1M[0].1n-F.1P}}1e{5(E.2u-A(u).1n()<F.1N){C=A(u).1n(A(u).1n()-F.1P)}5(A(2x).1d()-(E.2u-A(u).1n())<F.1N){C=A(u).1n(A(u).1n()+F.1P)}}5(C!==o){A.c.14.3P(B,E)}}});A.c.1k.1j("z","1T",{22:6(D,C){b B=A(4).p("z");B.1v=[];A(C.7.1T.5I!=8f?(C.7.1T.8a||":p(z)"):C.7.1T).1c(6(){b F=A(4);b E=F.m();5(4!=B.k[0]){B.1v.2U({3H:4,1d:F.5b(),19:F.5c(),j:E.j,h:E.h})}})},2a:6(P,K){b E=A(4).p("z");b Q=K.7.8b||20;b O=K.3T.h,N=O+E.1h.1d,D=K.3T.j,C=D+E.1h.19;2A(b M=E.1v.X-1;M>=0;M--){b L=E.1v[M].h,J=L+E.1v[M].1d,I=E.1v[M].j,R=I+E.1v[M].19;5(!((L-Q<O&&O<J+Q&&I-Q<D&&D<R+Q)||(L-Q<O&&O<J+Q&&I-Q<C&&C<R+Q)||(L-Q<N&&N<J+Q&&I-Q<D&&D<R+Q)||(L-Q<N&&N<J+Q&&I-Q<C&&C<R+Q))){5(E.1v[M].3M){(E.7.1T.5C&&E.7.1T.5C.12(E.k,15,A.1x(E.3k(),{5U:E.1v[M].3H})))}E.1v[M].3M=o;3O}5(K.7.5X!="8c"){b B=1o.1R(I-C)<=Q;b S=1o.1R(R-D)<=Q;b G=1o.1R(L-N)<=Q;b H=1o.1R(J-O)<=Q;5(B){K.w.j=E.1L("11",{j:I-E.1h.19,h:0}).j}5(S){K.w.j=E.1L("11",{j:R,h:0}).j}5(G){K.w.h=E.1L("11",{j:0,h:L-E.1h.1d}).h}5(H){K.w.h=E.1L("11",{j:0,h:J}).h}}b F=(B||S||G||H);5(K.7.5X!="8d"){b B=1o.1R(I-D)<=Q;b S=1o.1R(R-C)<=Q;b G=1o.1R(L-O)<=Q;b H=1o.1R(J-N)<=Q;5(B){K.w.j=E.1L("11",{j:I,h:0}).j}5(S){K.w.j=E.1L("11",{j:R-E.1h.19,h:0}).j}5(G){K.w.h=E.1L("11",{j:0,h:L}).h}5(H){K.w.h=E.1L("11",{j:0,h:J-E.1h.1d}).h}}5(!E.1v[M].3M&&(B||S||G||H||F)){(E.7.1T.1T&&E.7.1T.1T.12(E.k,15,A.1x(E.3k(),{5U:E.1v[M].3H})))}E.1v[M].3M=(B||S||G||H||F)}}});A.c.1k.1j("z","5M",{22:6(D,C){b B=A(4).p("z");B.3L=[];A(C.7.5M).1c(6(){5(A.p(4,"4d")){b E=A.p(4,"4d");B.3L.2U({q:E,5V:E.7.2q});E.7n();E.23("3z",D,B)}})},1W:6(D,C){b B=A(4).p("z");A.1c(B.3L,6(){5(4.q.2R){4.q.2R=0;B.3m=Y;4.q.3m=o;5(4.5V){4.q.7.2q=Y}4.q.3p(D);4.q.k.21("7m",[D,A.1x(4.q.c(),{7l:B.k})],4.q.7.7k);4.q.7.t=4.q.7.4i}1e{4.q.23("3D",D,B)}})},2a:6(F,E){b D=A(4).p("z"),B=4;b C=6(K){b H=K.h,J=H+K.1d,I=K.j,G=I+K.19;d(H<(4.1s.h+4.m.1a.h)&&(4.1s.h+4.m.1a.h)<J&&I<(4.1s.j+4.m.1a.j)&&(4.1s.j+4.m.1a.j)<G)};A.1c(D.3L,6(G){5(C.12(D,4.q.7o)){5(!4.q.2R){4.q.2R=1;4.q.1r=A(B).4l().1Y(4.q.k).p("4d-3H",Y);4.q.7.4i=4.q.7.t;4.q.7.t=6(){d E.t[0]};F.30=4.q.1r[0];4.q.3G(F,Y);4.q.3j(F,Y,Y);4.q.m.1a.j=D.m.1a.j;4.q.m.1a.h=D.m.1a.h;4.q.m.V.h-=D.m.V.h-4.q.m.V.h;4.q.m.V.j-=D.m.V.j-4.q.m.V.j;D.23("7t",F)}5(4.q.1r){4.q.2X(F)}}1e{5(4.q.2R){4.q.2R=0;4.q.3m=Y;4.q.7.2q=o;4.q.3p(F,Y);4.q.7.t=4.q.7.4i;4.q.1r.1B();5(4.q.5R){4.q.5R.1B()}D.23("7S",F)}}})}});A.c.1k.1j("z","2P",{22:6(D,B){b C=A.7i(A(B.7.2P.7b)).4n(6(F,E){d(1i(A(F).n("1K"),10)||B.7.2P.3a)-(1i(A(E).n("1K"),10)||B.7.2P.3a)});A(C).1c(6(E){4.24.1K=B.7.2P.3a+E});4[0].24.1K=B.7.2P.3a+C.X}})})(3b);(6(A){A.2g("c.1u",{2j:6(B,C){5(B=="1t"){4.7.1t=C&&A.3s(C)?C:6(D){d D.3W(1t)}}1e{A.2g.2y.2j.2W(4,4g)}},3f:6(){b C=4.7,B=C.1t;4.1E=0;4.2s=1;4.7.1t=4.7.1t&&A.3s(4.7.1t)?4.7.1t:6(D){d D.3W(B)};4.3l={1d:4.k[0].2K,19:4.k[0].2N};A.c.14.2t[4.7.2d]=A.c.14.2t[4.7.2d]||[];A.c.14.2t[4.7.2d].2U(4);(4.7.2O&&4.k.Z(4.7.2O+"-1u"))},2Y:{},c:6(B){d{z:(B.1r||B.k),t:B.t,w:B.w,3T:B.1s,7:4.7,k:4.k}},2f:6(){b B=A.c.14.2t[4.7.2d];2A(b C=0;C<B.X;C++){5(B[C]==4){B.54(C,1)}}4.k.1f("c-1u-v").2E("1u").1D(".1u")},4P:6(C){b B=A.c.14.2C;5(!B||(B.1r||B.k)[0]==4.k[0]){d}5(4.7.1t.12(4.k,(B.1r||B.k))){A.c.1k.12(4,"4V",[C,4.c(B)]);4.k.21("7c",[C,4.c(B)],4.7.4V)}},4O:6(C){b B=A.c.14.2C;5(!B||(B.1r||B.k)[0]==4.k[0]){d}5(4.7.1t.12(4.k,(B.1r||B.k))){A.c.1k.12(4,"4X",[C,4.c(B)]);4.k.21("7d",[C,4.c(B)],4.7.4X)}},4Y:6(E,B){b C=B||A.c.14.2C;5(!C||(C.1r||C.k)[0]==4.k[0]){d o}b D=o;4.k.2Z(":p(1u)").5S(".c-z-3E").1c(6(){b F=A.p(4,"1u");5(F.7.5x&&A.c.2I(C,A.1x(F,{m:F.k.m()}),F.7.3B)){D=Y;d o}});5(D){d o}5(4.7.1t.12(4.k,(C.1r||C.k))){A.c.1k.12(4,"2F",[E,4.c(C)]);4.k.21("2F",[E,4.c(C)],4.7.2F);d 4.k}d o},51:6(C){b B=A.c.14.2C;A.c.1k.12(4,"3z",[C,4.c(B)]);5(B){4.k.21("7g",[C,4.c(B)],4.7.3z)}},5g:6(C){b B=A.c.14.2C;A.c.1k.12(4,"3D",[C,4.c(B)]);5(B){4.k.21("7f",[C,4.c(B)],4.7.3D)}}});A.1x(A.c.1u,{2B:{v:o,3B:"2I",2d:"3r",2O:"c"}});A.c.2I=6(I,E,J){5(!E.m){d o}b D=(I.1s||I.w.1p).h,C=D+I.1h.1d,K=(I.1s||I.w.1p).j,H=K+I.1h.19;b F=E.m.h,B=F+E.3l.1d,L=E.m.j,G=L+E.3l.19;7u(J){3R"7v":d(F<D&&C<B&&L<K&&H<G);3n;3R"2I":d(F<D+(I.1h.1d/2)&&C-(I.1h.1d/2)<B&&L<K+(I.1h.19/2)&&H-(I.1h.19/2)<G);3n;3R"7K":d(F<((I.1s||I.w.1p).h+(I.3N||I.m.1a).h)&&((I.1s||I.w.1p).h+(I.3N||I.m.1a).h)<B&&L<((I.1s||I.w.1p).j+(I.3N||I.m.1a).j)&&((I.1s||I.w.1p).j+(I.3N||I.m.1a).j)<G);3n;3R"7Q":d((K>=L&&K<=G)||(H>=L&&H<=G)||(K<L&&H>G))&&((D>=F&&D<=B)||(C>=F&&C<=B)||(D<F&&C>B));3n;3r:d o;3n}};A.c.14={2C:15,2t:{"3r":[]},3P:6(E,H){b B=A.c.14.2t[E.7.2d];b F=H?H.4R:15;b G=(E.1r||E.k).2Z(":p(1u)").5K();5N:2A(b D=0;D<B.X;D++){5(B[D].7.v||(E&&!B[D].7.1t.12(B[D].k,(E.1r||E.k)))){3O}2A(b C=0;C<G.X;C++){5(G[C]==B[D].k[0]){B[D].3l.19=0;3O 5N}}B[D].2M=B[D].k.n("2G")!="3I";5(!B[D].2M){3O}B[D].m=B[D].k.m();B[D].3l={1d:B[D].k[0].2K,19:B[D].k[0].2N};5(F=="7H"||F=="7G"){B[D].51.12(B[D],H)}}},2F:6(B,C){b D=o;A.1c(A.c.14.2t[B.7.2d],6(){5(!4.7){d}5(!4.7.v&&4.2M&&A.c.2I(B,4,4.7.3B)){D=4.4Y.12(4,C)}5(!4.7.v&&4.2M&&4.7.1t.12(4.k,(B.1r||B.k))){4.2s=1;4.1E=0;4.5g.12(4,C)}});d D},2a:6(B,C){5(B.7.7y){A.c.14.3P(B,C)}A.1c(A.c.14.2t[B.7.2d],6(){5(4.7.v||4.5p||!4.2M){d}b D=A.c.2I(B,4,4.7.3B);b G=!D&&4.1E==1?"2s":(D&&4.1E==0?"1E":15);5(!G){d}b F;5(4.7.5x){b E=4.k.3X(":p(1u):1G(0)");5(E.X){F=A.p(E[0],"1u");F.5p=(G=="1E"?1:0)}}5(F&&G=="1E"){F.1E=0;F.2s=1;F.4O.12(F,C)}4[G]=1;4[G=="2s"?"1E":"2s"]=0;4[G=="1E"?"4P":"4O"].12(4,C);5(F&&G=="2s"){F.2s=0;F.1E=1;F.4P.12(F,C)}})}};A.c.1k.1j("1u","3C",{3z:6(C,B){A(4).Z(B.7.3C)},3D:6(C,B){A(4).1f(B.7.3C)},2F:6(C,B){A(4).1f(B.7.3C)}});A.c.1k.1j("1u","3Q",{4V:6(C,B){A(4).Z(B.7.3Q)},4X:6(C,B){A(4).1f(B.7.3Q)},2F:6(C,B){A(4).1f(B.7.3Q)}})})(3b);(6(A){A.2g("c.l",{3f:6(){4.7.2h+=".l";4.3o(Y)},2j:6(B,C){5((/^W/).18(B)){4.2L(C)}1e{4.7[B]=C;4.3o()}},X:6(){d 4.$l.X},4h:6(B){d B.5D&&B.5D.2J(/\\s/g,"4Q").2J(/[^A-7C-7D-9\\-4Q:\\.]/g,"")||4.7.59+A.p(B)},c:6(C,B){d{7:4.7,7E:C,5G:B,29:4.$l.29(C)}},3o:6(P){4.$1g=A("42:7F(a[1q])",4.k);4.$l=4.$1g.47(6(){d A("a",4)[0]});4.$1b=A([]);b O=4,E=4.7;4.$l.1c(6(R,Q){5(Q.25&&Q.25.2J("#","")){O.$1b=O.$1b.1j(Q.25)}1e{5(A(Q).1J("1q")!="#"){A.p(Q,"1q.l",Q.1q);A.p(Q,"1z.l",Q.1q);b T=O.4h(Q);Q.1q="#"+T;b S=A("#"+T);5(!S.X){S=A(E.4p).1J("3u",T).Z(E.3h).7B(O.$1b[R-1]||O.k);S.p("2f.l",Y)}O.$1b=O.$1b.1j(S)}1e{E.v.2U(R+1)}}});5(P){4.k.Z(E.4D);4.$1b.1c(6(){b Q=A(4);Q.Z(E.3h)});5(E.W===1Q){5(4W.25){4.$l.1c(6(T,Q){5(Q.25==4W.25){E.W=T;5(A.1I.2k||A.1I.7A){b S=A(4W.25),R=S.1J("3u");S.1J("3u","");3U(6(){S.1J("3u",R)},5z)}7w(0,0);d o}})}1e{5(E.2e){b I=1i(A.2e("c-l-"+A.p(O.k[0])),10);5(I&&O.$l[I]){E.W=I}}1e{5(O.$1g.1S("."+E.1l).X){E.W=O.$1g.29(O.$1g.1S("."+E.1l)[0])}}}}E.W=E.W===15||E.W!==1Q?E.W:0;E.v=A.7x(E.v.5l(A.47(4.$1g.1S("."+E.2z),6(R,Q){d O.$1g.29(R)}))).4n();5(A.3q(E.W,E.v)!=-1){E.v.54(A.3q(E.W,E.v),1)}4.$1b.Z(E.2r);4.$1g.1f(E.1l);5(E.W!==15){4.$1b.1G(E.W).4S().1f(E.2r);4.$1g.1G(E.W).Z(E.1l);b B=6(){O.2b("4S",15,O.c(O.$l[E.W],O.$1b[E.W]))};5(A.p(4.$l[E.W],"1z.l")){4.1z(E.W,B)}1e{B()}}A(2x).1A("7z",6(){O.$l.1D(".l");O.$1g=O.$l=O.$1b=15})}1e{E.W=4.$1g.29(4.$1g.1S("."+E.1l)[0])}5(E.2e){A.2e("c-l-"+A.p(O.k[0]),E.W,E.2e)}2A(b H=0,N;N=4.$1g[H];H++){A(N)[A.3q(H,E.v)!=-1&&!A(N).31(E.1l)?"Z":"1f"](E.2z)}5(E.2l===o){4.$l.2E("2l.l")}b J,D,K={"3a-1d":0,4T:1},F="7O";5(E.2Q&&E.2Q.5I==5f){J=E.2Q[0]||K,D=E.2Q[1]||K}1e{J=D=E.2Q||K}b C={2G:"",1F:"",19:""};5(!A.1I.2k){C.1O=""}6 M(R,Q,S){Q.4U(J,J.4T||F,6(){Q.Z(E.2r).n(C);5(A.1I.2k&&J.1O){Q[0].24.1S=""}5(S){L(R,S,Q)}})}6 L(R,S,Q){5(D===K){S.n("2G","44")}S.4U(D,D.4T||F,6(){S.1f(E.2r).n(C);5(A.1I.2k&&D.1O){S[0].24.1S=""}O.2b("4S",15,O.c(R,S[0]))})}6 G(R,T,Q,S){T.Z(E.1l).7P().1f(E.1l);M(R,Q,S)}4.$l.1D(".l").1A(E.2h,6(){b T=A(4).3X("42:1G(0)"),Q=O.$1b.1S(":2M"),S=A(4.25);5((T.31(E.1l)&&!E.3Z)||T.31(E.2z)||A(4).31(E.3t)||O.2b("2L",15,O.c(4,S[0]))===o){4.48();d o}O.7.W=O.$l.29(4);5(E.3Z){5(T.31(E.1l)){O.7.W=15;T.1f(E.1l);O.$1b.1W();M(4,Q);4.48();d o}1e{5(!Q.X){O.$1b.1W();b R=4;O.1z(O.$l.29(4),6(){T.Z(E.1l).Z(E.4H);L(R,S)});4.48();d o}}}5(E.2e){A.2e("c-l-"+A.p(O.k[0]),O.7.W,E.2e)}O.$1b.1W();5(S.X){b R=4;O.1z(O.$l.29(4),Q.X?6(){G(R,T,Q,S)}:6(){T.Z(E.1l);L(R,S)})}1e{7N"3b 7M 7I: 7J 7L 7e."}5(A.1I.2k){4.48()}d o});5(!(/^1a/).18(E.2h)){4.$l.1A("1a.l",6(){d o})}},1j:6(E,D,C){5(C==1Q){C=4.$l.X}b G=4.7;b I=A(G.58.2J(/#\\{1q\\}/g,E).2J(/#\\{43\\}/g,D));I.p("2f.l",Y);b H=E.7h("#")==0?E.2J("#",""):4.4h(A("a:78-79",I)[0]);b F=A("#"+H);5(!F.X){F=A(G.4p).1J("3u",H).Z(G.2r).p("2f.l",Y)}F.Z(G.3h);5(C>=4.$1g.X){I.1Y(4.k);F.1Y(4.k[0].1X)}1e{I.5L(4.$1g[C]);F.5L(4.$1b[C])}G.v=A.47(G.v,6(K,J){d K>=C?++K:K});4.3o();5(4.$l.X==1){I.Z(G.1l);F.1f(G.2r);b B=A.p(4.$l[0],"1z.l");5(B){4.1z(C,B)}}4.2b("1j",15,4.c(4.$l[C],4.$1b[C]))},1B:6(B){b D=4.7,E=4.$1g.1G(B).1B(),C=4.$1b.1G(B).1B();5(E.31(D.1l)&&4.$l.X>1){4.2L(B+(B+1<4.$l.X?1:-1))}D.v=A.47(A.5Q(D.v,6(G,F){d G!=B}),6(G,F){d G>=B?--G:G});4.3o();4.2b("1B",15,4.c(E.2Z("a")[0],C[0]))},4j:6(B){b C=4.7;5(A.3q(B,C.v)==-1){d}b D=4.$1g.1G(B).1f(C.2z);5(A.1I.7a){D.n("2G","7j-44");3U(6(){D.n("2G","44")},0)}C.v=A.5Q(C.v,6(F,E){d F!=B});4.2b("4j",15,4.c(4.$l[B],4.$1b[B]))},4m:6(C){b B=4,D=4.7;5(C!=D.W){4.$1g.1G(C).Z(D.2z);D.v.2U(C);D.v.4n();4.2b("4m",15,4.c(4.$l[C],4.$1b[C]))}},2L:6(B){5(2T B=="2S"){B=4.$l.29(4.$l.1S("[1q$="+B+"]")[0])}4.$l.1G(B).7r(4.7.2h)},1z:6(G,K){b L=4,D=4.7,E=4.$l.1G(G),J=E[0],H=K==1Q||K===o,B=E.p("1z.l");K=K||6(){};5(!B||!H&&A.p(J,"2l.l")){K();d}b M=6(N){b O=A(N),P=O.2Z("*:7s");d P.X&&P.3W(":5S(7p)")&&P||O};b C=6(){L.$l.1S("."+D.3t).1f(D.3t).1c(6(){5(D.41){M(4).V().2o(M(4).p("43.l"))}});L.3V=15};5(D.41){b I=M(J).2o();M(J).7R("<4o></4o>").2Z("4o").p("43.l",I).2o(D.41)}b F=A.1x({},D.3Y,{5i:B,49:6(O,N){A(J.25).2o(O);C();5(D.2l){A.p(J,"2l.l",Y)}L.2b("1z",15,L.c(L.$l[G],L.$1b[G]));D.3Y.49&&D.3Y.49(O,N);K()}});5(4.3V){4.3V.8e();C()}E.Z(D.3t);3U(6(){L.3V=A.8g(F)},0)},5i:6(C,B){4.$l.1G(C).2E("2l.l").p("1z.l",B)},2f:6(){b B=4.7;4.k.1D(".l").1f(B.4D).2E("l");4.$l.1c(6(){b C=A.p(4,"1q.l");5(C){4.1q=C}b D=A(4).1D(".l");A.1c(["1q","1z","2l"],6(F,E){D.2E(E+".l")})});4.$1g.1j(4.$1b).1c(6(){5(A.p(4,"2f.l")){A(4).1B()}1e{A(4).1f([B.1l,B.4H,B.2z,B.3h,B.2r].88(" "))}})}});A.c.l.2B={3Z:o,2h:"1a",v:[],2e:15,41:"7Y&#7Z;",2l:o,59:"c-l-",3Y:{},2Q:15,58:\'<42><a 1q="#{1q}"><50>#{43}</50></a></42>\',4p:"<2V></2V>",4D:"c-l-89",1l:"c-l-W",4H:"c-l-3Z",2z:"c-l-v",3h:"c-l-5G",2r:"c-l-86",3t:"c-l-87"};A.c.l.5m="X";A.1x(A.c.l.2y,{4t:15,85:6(C,F){F=F||o;b B=4,E=4.7.W;6 G(){B.4t=84(6(){E=++E<B.$l.X?E:0;B.2L(E)},C)}6 D(H){5(!H||H.83){6p(B.4t)}}5(C){G();5(!F){4.$l.1A(4.7.2h,D)}1e{4.$l.1A(4.7.2h,6(){D();E=B.7.W;G()})}}1e{D();4.$l.1D(4.7.2h,D)}}})})(3b);',62,513,'||||this|if|function|options||||var|ui|return||||left||top|element|tabs|offset|css|false|data|instance|||helper|document|disabled|position|||draggable|||||||||||||||||||||containment|parent|selected|length|true|addClass||relative|call||ddmanager|null|||test|height|click|panels|each|width|else|removeClass|lis|helperProportions|parseInt|add|plugin|selectedClass|scrollTop|scrollLeft|Math|absolute|href|currentItem|positionAbs|accept|droppable|snapElements|margins|extend|body|load|bind|remove|cssPosition|unbind|isover|overflow|eq|overflowY|browser|attr|zIndex|_convertPositionTo|overflowX|scrollSensitivity|opacity|scrollSpeed|undefined|abs|filter|snap|offsetParent|tagName|stop|parentNode|appendTo|grid||triggerHandler|start|_propagate|style|hash|fixed||_mouseStarted|index|drag|_trigger|scroll|scope|cookie|destroy|widget|event|auto|_setData|msie|cache|pageY|scrollLeftParent|html|scrollTopParent|revert|hideClass|isout|droppables|pageX|widgetName|originalPosition|window|prototype|disabledClass|for|defaults|current|cursor|removeData|drop|display|handle|intersect|replace|offsetWidth|select|visible|offsetHeight|cssNamespace|stack|fx|isOver|string|typeof|push|div|apply|_mouseDrag|plugins|find|target|hasClass|||||||||min|jQuery|unselectable|iframeFix|cssCache|_init|axis|panelClass|_mouseUp|_mouseStart|uiHash|proportions|cancelHelperRemoval|break|_tabify|_mouseStop|inArray|default|isFunction|loadingClass|id|px|while|delay|HTML|activate|do|tolerance|activeClass|deactivate|dragging|hidden|_mouseCapture|item|none|_mouseDownEvent|mouseDelayMet|sortables|snapping|clickOffset|continue|prepareOffsets|hoverClass|case|cancel|absolutePosition|setTimeout|xhr|is|parents|ajaxOptions|unselect||spinner|li|label|block|||map|blur|success|get|overflowXOffset|overflowYOffset|sortable|fn|split|arguments|_tabId|_helper|enable|_getData|clone|disable|sort|em|panelTemplate|mouse|PAGEX_INCLUDES_SCROLL|OFFSET_PARENT_NOT_SCROLL_PARENT_X|rotation|OFFSET_PARENT_NOT_SCROLL_PARENT_Y|PAGEY_INCLUDES_SCROLL|cacheHelperProportions|borderLeftWidth|max|borderTopWidth|_mouseDelayMet|_mouseDistanceMet|original|navClass|_cursor|_zIndex|distance|unselectClass|_mouseUpDelegate|_generatePosition|_mouseMoveDelegate|_clear|_opacity|widgetEventPrefix|_out|_over|_|type|show|duration|animate|over|location|out|_drop|_mouseUnselectable|span|_activate|removeChild|_mouseDown|splice|_mouseMove|class|adjustOffsetFromHelper|tabTemplate|idPrefix|createHelper|outerWidth|outerHeight|cursorAt|cacheScrollParents|Array|_deactivate|selectstart|url|MozUserSelect|on|concat|getter|right|round|greedyChild|bottom|_mouseInit|setContainment|marginRight|scrollHeight|marginBottom|mouseup|greedy|_mouseDestroy|500|input|button|release|title|dropBehaviour|mousemove|panel|getHandle|constructor|widgetBaseClass|andSelf|insertBefore|connectToSortable|droppablesLoop|option|getterSetter|grep|placeholder|not|metadata|snapItem|shouldRevert|5000px|snapMode|TAB|NUMPAD_MULTIPLY|106|toLowerCase|nodeName|SPACE|tabIndex|NUMPAD_DECIMAL|marginLeft|textarea|LEFT|NUMPAD_ADD|slice|110|107|UP|expr|RIGHT|visibility|SHIFT|111|resizable|190|PAGE_UP|dir|PAGE_DOWN|NUMPAD_SUBTRACT|clearInterval|PERIOD|NUMPAD_ENTER|NUMPAD_DIVIDE|INSERT|109|tabbable|BACKSPACE|DOWN|108|in|hasScroll|mousedown|CONTROL|DELETE|fix|gen|off|transparent|rgba|try|backgroundImage|catch|enableSelection|disableSelection|marginTop|started|eventPrefix|backgroundColor|CAPS_LOCK|new|keyCode|ESCAPE|substring|COMMA|ENTER|188|which|getData|_mouseDelayTimer|preventDefault|setData|END|HOME|revertDuration|first|child|safari|group|dropover|dropout|identifier|dropdeactivate|dropactivate|indexOf|makeArray|inline|receive|sender|sortreceive|_refreshItems|containerCache|img|mozilla|trigger|last|toSortable|switch|fit|scrollTo|unique|refreshPositions|unload|opera|insertAfter|Za|z0|tab|has|sortactivate|dragstart|Tabs|Mismatching|pointer|fragment|UI|throw|normal|siblings|touch|wrapInner|fromSortable|fff|background|iframe|001|1000|Loading|8230|valid|invalid|scrollWidth|clientX|setInterval|rotate|hide|loading|join|nav|items|snapTolerance|inner|outer|abort|String|ajax'.split('|'),0,{}))
diff --git a/data/static/js/jquery.hotkeys-0.7.9.min.js b/data/static/js/jquery.hotkeys-0.7.9.min.js
new file mode 100644
--- /dev/null
+++ b/data/static/js/jquery.hotkeys-0.7.9.min.js
@@ -0,0 +1,19 @@
+(function(jQuery){jQuery.fn.__bind__=jQuery.fn.bind;jQuery.fn.__unbind__=jQuery.fn.unbind;jQuery.fn.__find__=jQuery.fn.find;var hotkeys={version:'0.7.9',override:/keypress|keydown|keyup/g,triggersMap:{},specialKeys:{27:'esc',9:'tab',32:'space',13:'return',8:'backspace',145:'scroll',20:'capslock',144:'numlock',19:'pause',45:'insert',36:'home',46:'del',35:'end',33:'pageup',34:'pagedown',37:'left',38:'up',39:'right',40:'down',109:'-',112:'f1',113:'f2',114:'f3',115:'f4',116:'f5',117:'f6',118:'f7',119:'f8',120:'f9',121:'f10',122:'f11',123:'f12',191:'/'},shiftNums:{"`":"~","1":"!","2":"@","3":"#","4":"$","5":"%","6":"^","7":"&","8":"*","9":"(","0":")","-":"_","=":"+",";":":","'":"\"",",":"<",".":">","/":"?","\\":"|"},newTrigger:function(type,combi,callback){var result={};result[type]={};result[type][combi]={cb:callback,disableInInput:false};return result;}};hotkeys.specialKeys=jQuery.extend(hotkeys.specialKeys,{96:'0',97:'1',98:'2',99:'3',100:'4',101:'5',102:'6',103:'7',104:'8',105:'9',106:'*',107:'+',109:'-',110:'.',111:'/'});jQuery.fn.find=function(selector){this.query=selector;return jQuery.fn.__find__.apply(this,arguments);};jQuery.fn.unbind=function(type,combi,fn){if(jQuery.isFunction(combi)){fn=combi;combi=null;}
+if(combi&&typeof combi==='string'){var selectorId=((this.prevObject&&this.prevObject.query)||(this[0].id&&this[0].id)||this[0]).toString();var hkTypes=type.split(' ');for(var x=0;x<hkTypes.length;x++){delete hotkeys.triggersMap[selectorId][hkTypes[x]][combi];}}
+return this.__unbind__(type,fn);};jQuery.fn.bind=function(type,data,fn){var handle=type.match(hotkeys.override);if(jQuery.isFunction(data)||!handle){return this.__bind__(type,data,fn);}
+else{var result=null,pass2jq=jQuery.trim(type.replace(hotkeys.override,''));if(pass2jq){result=this.__bind__(pass2jq,data,fn);}
+if(typeof data==="string"){data={'combi':data};}
+if(data.combi){for(var x=0;x<handle.length;x++){var eventType=handle[x];var combi=data.combi.toLowerCase(),trigger=hotkeys.newTrigger(eventType,combi,fn),selectorId=((this.prevObject&&this.prevObject.query)||(this[0].id&&this[0].id)||this[0]).toString();trigger[eventType][combi].disableInInput=data.disableInInput;if(!hotkeys.triggersMap[selectorId]){hotkeys.triggersMap[selectorId]=trigger;}
+else if(!hotkeys.triggersMap[selectorId][eventType]){hotkeys.triggersMap[selectorId][eventType]=trigger[eventType];}
+var mapPoint=hotkeys.triggersMap[selectorId][eventType][combi];if(!mapPoint){hotkeys.triggersMap[selectorId][eventType][combi]=[trigger[eventType][combi]];}
+else if(mapPoint.constructor!==Array){hotkeys.triggersMap[selectorId][eventType][combi]=[mapPoint];}
+else{hotkeys.triggersMap[selectorId][eventType][combi][mapPoint.length]=trigger[eventType][combi];}
+this.each(function(){var jqElem=jQuery(this);if(jqElem.attr('hkId')&&jqElem.attr('hkId')!==selectorId){selectorId=jqElem.attr('hkId')+";"+selectorId;}
+jqElem.attr('hkId',selectorId);});result=this.__bind__(handle.join(' '),data,hotkeys.handler)}}
+return result;}};hotkeys.findElement=function(elem){if(!jQuery(elem).attr('hkId')){if(jQuery.browser.opera||jQuery.browser.safari){while(!jQuery(elem).attr('hkId')&&elem.parentNode){elem=elem.parentNode;}}}
+return elem;};hotkeys.handler=function(event){var target=hotkeys.findElement(event.currentTarget),jTarget=jQuery(target),ids=jTarget.attr('hkId');if(ids){ids=ids.split(';');var code=event.which,type=event.type,special=hotkeys.specialKeys[code],character=!special&&String.fromCharCode(code).toLowerCase(),shift=event.shiftKey,ctrl=event.ctrlKey,alt=event.altKey||event.originalEvent.altKey,mapPoint=null;for(var x=0;x<ids.length;x++){if(hotkeys.triggersMap[ids[x]][type]){mapPoint=hotkeys.triggersMap[ids[x]][type];break;}}
+if(mapPoint){var trigger;if(!shift&&!ctrl&&!alt){trigger=mapPoint[special]||(character&&mapPoint[character]);}
+else{var modif='';if(alt)modif+='alt+';if(ctrl)modif+='ctrl+';if(shift)modif+='shift+';trigger=mapPoint[modif+special];if(!trigger){if(character){trigger=mapPoint[modif+character]||mapPoint[modif+hotkeys.shiftNums[character]]||(modif==='shift+'&&mapPoint[hotkeys.shiftNums[character]]);}}}
+if(trigger){var result=false;for(var x=0;x<trigger.length;x++){if(trigger[x].disableInInput){var elem=jQuery(event.target);if(jTarget.is("input")||jTarget.is("textarea")||jTarget.is("select")||elem.is("input")||elem.is("textarea")||elem.is("select")){return true;}}
+result=result||trigger[x].cb.apply(this,[event]);}
+return result;}}}};window.hotkeys=hotkeys;return jQuery;})(jQuery);
diff --git a/data/static/js/jquery.min.js b/data/static/js/jquery.min.js
new file mode 100644
--- /dev/null
+++ b/data/static/js/jquery.min.js
@@ -0,0 +1,32 @@
+/*
+ * jQuery 1.2.6 - New Wave Javascript
+ *
+ * Copyright (c) 2008 John Resig (jquery.com)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2008-05-24 14:22:17 -0400 (Sat, 24 May 2008) $
+ * $Rev: 5685 $
+ */
+(function(){var _jQuery=window.jQuery,_$=window.$;var jQuery=window.jQuery=window.$=function(selector,context){return new jQuery.fn.init(selector,context);};var quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/,isSimple=/^.[^:#\[\.]*$/,undefined;jQuery.fn=jQuery.prototype={init:function(selector,context){selector=selector||document;if(selector.nodeType){this[0]=selector;this.length=1;return this;}if(typeof selector=="string"){var match=quickExpr.exec(selector);if(match&&(match[1]||!context)){if(match[1])selector=jQuery.clean([match[1]],context);else{var elem=document.getElementById(match[3]);if(elem){if(elem.id!=match[3])return jQuery().find(selector);return jQuery(elem);}selector=[];}}else
+return jQuery(context).find(selector);}else if(jQuery.isFunction(selector))return jQuery(document)[jQuery.fn.ready?"ready":"load"](selector);return this.setArray(jQuery.makeArray(selector));},jquery:"1.2.6",size:function(){return this.length;},length:0,get:function(num){return num==undefined?jQuery.makeArray(this):this[num];},pushStack:function(elems){var ret=jQuery(elems);ret.prevObject=this;return ret;},setArray:function(elems){this.length=0;Array.prototype.push.apply(this,elems);return this;},each:function(callback,args){return jQuery.each(this,callback,args);},index:function(elem){var ret=-1;return jQuery.inArray(elem&&elem.jquery?elem[0]:elem,this);},attr:function(name,value,type){var options=name;if(name.constructor==String)if(value===undefined)return this[0]&&jQuery[type||"attr"](this[0],name);else{options={};options[name]=value;}return this.each(function(i){for(name in options)jQuery.attr(type?this.style:this,name,jQuery.prop(this,options[name],type,i,name));});},css:function(key,value){if((key=='width'||key=='height')&&parseFloat(value)<0)value=undefined;return this.attr(key,value,"curCSS");},text:function(text){if(typeof text!="object"&&text!=null)return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(text));var ret="";jQuery.each(text||this,function(){jQuery.each(this.childNodes,function(){if(this.nodeType!=8)ret+=this.nodeType!=1?this.nodeValue:jQuery.fn.text([this]);});});return ret;},wrapAll:function(html){if(this[0])jQuery(html,this[0].ownerDocument).clone().insertBefore(this[0]).map(function(){var elem=this;while(elem.firstChild)elem=elem.firstChild;return elem;}).append(this);return this;},wrapInner:function(html){return this.each(function(){jQuery(this).contents().wrapAll(html);});},wrap:function(html){return this.each(function(){jQuery(this).wrapAll(html);});},append:function(){return this.domManip(arguments,true,false,function(elem){if(this.nodeType==1)this.appendChild(elem);});},prepend:function(){return this.domManip(arguments,true,true,function(elem){if(this.nodeType==1)this.insertBefore(elem,this.firstChild);});},before:function(){return this.domManip(arguments,false,false,function(elem){this.parentNode.insertBefore(elem,this);});},after:function(){return this.domManip(arguments,false,true,function(elem){this.parentNode.insertBefore(elem,this.nextSibling);});},end:function(){return this.prevObject||jQuery([]);},find:function(selector){var elems=jQuery.map(this,function(elem){return jQuery.find(selector,elem);});return this.pushStack(/[^+>] [^+>]/.test(selector)||selector.indexOf("..")>-1?jQuery.unique(elems):elems);},clone:function(events){var ret=this.map(function(){if(jQuery.browser.msie&&!jQuery.isXMLDoc(this)){var clone=this.cloneNode(true),container=document.createElement("div");container.appendChild(clone);return jQuery.clean([container.innerHTML])[0];}else
+return this.cloneNode(true);});var clone=ret.find("*").andSelf().each(function(){if(this[expando]!=undefined)this[expando]=null;});if(events===true)this.find("*").andSelf().each(function(i){if(this.nodeType==3)return;var events=jQuery.data(this,"events");for(var type in events)for(var handler in events[type])jQuery.event.add(clone[i],type,events[type][handler],events[type][handler].data);});return ret;},filter:function(selector){return this.pushStack(jQuery.isFunction(selector)&&jQuery.grep(this,function(elem,i){return selector.call(elem,i);})||jQuery.multiFilter(selector,this));},not:function(selector){if(selector.constructor==String)if(isSimple.test(selector))return this.pushStack(jQuery.multiFilter(selector,this,true));else
+selector=jQuery.multiFilter(selector,this);var isArrayLike=selector.length&&selector[selector.length-1]!==undefined&&!selector.nodeType;return this.filter(function(){return isArrayLike?jQuery.inArray(this,selector)<0:this!=selector;});},add:function(selector){return this.pushStack(jQuery.unique(jQuery.merge(this.get(),typeof selector=='string'?jQuery(selector):jQuery.makeArray(selector))));},is:function(selector){return!!selector&&jQuery.multiFilter(selector,this).length>0;},hasClass:function(selector){return this.is("."+selector);},val:function(value){if(value==undefined){if(this.length){var elem=this[0];if(jQuery.nodeName(elem,"select")){var index=elem.selectedIndex,values=[],options=elem.options,one=elem.type=="select-one";if(index<0)return null;for(var i=one?index:0,max=one?index+1:options.length;i<max;i++){var option=options[i];if(option.selected){value=jQuery.browser.msie&&!option.attributes.value.specified?option.text:option.value;if(one)return value;values.push(value);}}return values;}else
+return(this[0].value||"").replace(/\r/g,"");}return undefined;}if(value.constructor==Number)value+='';return this.each(function(){if(this.nodeType!=1)return;if(value.constructor==Array&&/radio|checkbox/.test(this.type))this.checked=(jQuery.inArray(this.value,value)>=0||jQuery.inArray(this.name,value)>=0);else if(jQuery.nodeName(this,"select")){var values=jQuery.makeArray(value);jQuery("option",this).each(function(){this.selected=(jQuery.inArray(this.value,values)>=0||jQuery.inArray(this.text,values)>=0);});if(!values.length)this.selectedIndex=-1;}else
+this.value=value;});},html:function(value){return value==undefined?(this[0]?this[0].innerHTML:null):this.empty().append(value);},replaceWith:function(value){return this.after(value).remove();},eq:function(i){return this.slice(i,i+1);},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments));},map:function(callback){return this.pushStack(jQuery.map(this,function(elem,i){return callback.call(elem,i,elem);}));},andSelf:function(){return this.add(this.prevObject);},data:function(key,value){var parts=key.split(".");parts[1]=parts[1]?"."+parts[1]:"";if(value===undefined){var data=this.triggerHandler("getData"+parts[1]+"!",[parts[0]]);if(data===undefined&&this.length)data=jQuery.data(this[0],key);return data===undefined&&parts[1]?this.data(parts[0]):data;}else
+return this.trigger("setData"+parts[1]+"!",[parts[0],value]).each(function(){jQuery.data(this,key,value);});},removeData:function(key){return this.each(function(){jQuery.removeData(this,key);});},domManip:function(args,table,reverse,callback){var clone=this.length>1,elems;return this.each(function(){if(!elems){elems=jQuery.clean(args,this.ownerDocument);if(reverse)elems.reverse();}var obj=this;if(table&&jQuery.nodeName(this,"table")&&jQuery.nodeName(elems[0],"tr"))obj=this.getElementsByTagName("tbody")[0]||this.appendChild(this.ownerDocument.createElement("tbody"));var scripts=jQuery([]);jQuery.each(elems,function(){var elem=clone?jQuery(this).clone(true)[0]:this;if(jQuery.nodeName(elem,"script"))scripts=scripts.add(elem);else{if(elem.nodeType==1)scripts=scripts.add(jQuery("script",elem).remove());callback.call(obj,elem);}});scripts.each(evalScript);});}};jQuery.fn.init.prototype=jQuery.fn;function evalScript(i,elem){if(elem.src)jQuery.ajax({url:elem.src,async:false,dataType:"script"});else
+jQuery.globalEval(elem.text||elem.textContent||elem.innerHTML||"");if(elem.parentNode)elem.parentNode.removeChild(elem);}function now(){return+new Date;}jQuery.extend=jQuery.fn.extend=function(){var target=arguments[0]||{},i=1,length=arguments.length,deep=false,options;if(target.constructor==Boolean){deep=target;target=arguments[1]||{};i=2;}if(typeof target!="object"&&typeof target!="function")target={};if(length==i){target=this;--i;}for(;i<length;i++)if((options=arguments[i])!=null)for(var name in options){var src=target[name],copy=options[name];if(target===copy)continue;if(deep&&copy&&typeof copy=="object"&&!copy.nodeType)target[name]=jQuery.extend(deep,src||(copy.length!=null?[]:{}),copy);else if(copy!==undefined)target[name]=copy;}return target;};var expando="jQuery"+now(),uuid=0,windowData={},exclude=/z-?index|font-?weight|opacity|zoom|line-?height/i,defaultView=document.defaultView||{};jQuery.extend({noConflict:function(deep){window.$=_$;if(deep)window.jQuery=_jQuery;return jQuery;},isFunction:function(fn){return!!fn&&typeof fn!="string"&&!fn.nodeName&&fn.constructor!=Array&&/^[\s[]?function/.test(fn+"");},isXMLDoc:function(elem){return elem.documentElement&&!elem.body||elem.tagName&&elem.ownerDocument&&!elem.ownerDocument.body;},globalEval:function(data){data=jQuery.trim(data);if(data){var head=document.getElementsByTagName("head")[0]||document.documentElement,script=document.createElement("script");script.type="text/javascript";if(jQuery.browser.msie)script.text=data;else
+script.appendChild(document.createTextNode(data));head.insertBefore(script,head.firstChild);head.removeChild(script);}},nodeName:function(elem,name){return elem.nodeName&&elem.nodeName.toUpperCase()==name.toUpperCase();},cache:{},data:function(elem,name,data){elem=elem==window?windowData:elem;var id=elem[expando];if(!id)id=elem[expando]=++uuid;if(name&&!jQuery.cache[id])jQuery.cache[id]={};if(data!==undefined)jQuery.cache[id][name]=data;return name?jQuery.cache[id][name]:id;},removeData:function(elem,name){elem=elem==window?windowData:elem;var id=elem[expando];if(name){if(jQuery.cache[id]){delete jQuery.cache[id][name];name="";for(name in jQuery.cache[id])break;if(!name)jQuery.removeData(elem);}}else{try{delete elem[expando];}catch(e){if(elem.removeAttribute)elem.removeAttribute(expando);}delete jQuery.cache[id];}},each:function(object,callback,args){var name,i=0,length=object.length;if(args){if(length==undefined){for(name in object)if(callback.apply(object[name],args)===false)break;}else
+for(;i<length;)if(callback.apply(object[i++],args)===false)break;}else{if(length==undefined){for(name in object)if(callback.call(object[name],name,object[name])===false)break;}else
+for(var value=object[0];i<length&&callback.call(value,i,value)!==false;value=object[++i]){}}return object;},prop:function(elem,value,type,i,name){if(jQuery.isFunction(value))value=value.call(elem,i);return value&&value.constructor==Number&&type=="curCSS"&&!exclude.test(name)?value+"px":value;},className:{add:function(elem,classNames){jQuery.each((classNames||"").split(/\s+/),function(i,className){if(elem.nodeType==1&&!jQuery.className.has(elem.className,className))elem.className+=(elem.className?" ":"")+className;});},remove:function(elem,classNames){if(elem.nodeType==1)elem.className=classNames!=undefined?jQuery.grep(elem.className.split(/\s+/),function(className){return!jQuery.className.has(classNames,className);}).join(" "):"";},has:function(elem,className){return jQuery.inArray(className,(elem.className||elem).toString().split(/\s+/))>-1;}},swap:function(elem,options,callback){var old={};for(var name in options){old[name]=elem.style[name];elem.style[name]=options[name];}callback.call(elem);for(var name in options)elem.style[name]=old[name];},css:function(elem,name,force){if(name=="width"||name=="height"){var val,props={position:"absolute",visibility:"hidden",display:"block"},which=name=="width"?["Left","Right"]:["Top","Bottom"];function getWH(){val=name=="width"?elem.offsetWidth:elem.offsetHeight;var padding=0,border=0;jQuery.each(which,function(){padding+=parseFloat(jQuery.curCSS(elem,"padding"+this,true))||0;border+=parseFloat(jQuery.curCSS(elem,"border"+this+"Width",true))||0;});val-=Math.round(padding+border);}if(jQuery(elem).is(":visible"))getWH();else
+jQuery.swap(elem,props,getWH);return Math.max(0,val);}return jQuery.curCSS(elem,name,force);},curCSS:function(elem,name,force){var ret,style=elem.style;function color(elem){if(!jQuery.browser.safari)return false;var ret=defaultView.getComputedStyle(elem,null);return!ret||ret.getPropertyValue("color")=="";}if(name=="opacity"&&jQuery.browser.msie){ret=jQuery.attr(style,"opacity");return ret==""?"1":ret;}if(jQuery.browser.opera&&name=="display"){var save=style.outline;style.outline="0 solid black";style.outline=save;}if(name.match(/float/i))name=styleFloat;if(!force&&style&&style[name])ret=style[name];else if(defaultView.getComputedStyle){if(name.match(/float/i))name="float";name=name.replace(/([A-Z])/g,"-$1").toLowerCase();var computedStyle=defaultView.getComputedStyle(elem,null);if(computedStyle&&!color(elem))ret=computedStyle.getPropertyValue(name);else{var swap=[],stack=[],a=elem,i=0;for(;a&&color(a);a=a.parentNode)stack.unshift(a);for(;i<stack.length;i++)if(color(stack[i])){swap[i]=stack[i].style.display;stack[i].style.display="block";}ret=name=="display"&&swap[stack.length-1]!=null?"none":(computedStyle&&computedStyle.getPropertyValue(name))||"";for(i=0;i<swap.length;i++)if(swap[i]!=null)stack[i].style.display=swap[i];}if(name=="opacity"&&ret=="")ret="1";}else if(elem.currentStyle){var camelCase=name.replace(/\-(\w)/g,function(all,letter){return letter.toUpperCase();});ret=elem.currentStyle[name]||elem.currentStyle[camelCase];if(!/^\d+(px)?$/i.test(ret)&&/^\d/.test(ret)){var left=style.left,rsLeft=elem.runtimeStyle.left;elem.runtimeStyle.left=elem.currentStyle.left;style.left=ret||0;ret=style.pixelLeft+"px";style.left=left;elem.runtimeStyle.left=rsLeft;}}return ret;},clean:function(elems,context){var ret=[];context=context||document;if(typeof context.createElement=='undefined')context=context.ownerDocument||context[0]&&context[0].ownerDocument||document;jQuery.each(elems,function(i,elem){if(!elem)return;if(elem.constructor==Number)elem+='';if(typeof elem=="string"){elem=elem.replace(/(<(\w+)[^>]*?)\/>/g,function(all,front,tag){return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?all:front+"></"+tag+">";});var tags=jQuery.trim(elem).toLowerCase(),div=context.createElement("div");var wrap=!tags.indexOf("<opt")&&[1,"<select multiple='multiple'>","</select>"]||!tags.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||tags.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!tags.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!tags.indexOf("<td")||!tags.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!tags.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||jQuery.browser.msie&&[1,"div<div>","</div>"]||[0,"",""];div.innerHTML=wrap[1]+elem+wrap[2];while(wrap[0]--)div=div.lastChild;if(jQuery.browser.msie){var tbody=!tags.indexOf("<table")&&tags.indexOf("<tbody")<0?div.firstChild&&div.firstChild.childNodes:wrap[1]=="<table>"&&tags.indexOf("<tbody")<0?div.childNodes:[];for(var j=tbody.length-1;j>=0;--j)if(jQuery.nodeName(tbody[j],"tbody")&&!tbody[j].childNodes.length)tbody[j].parentNode.removeChild(tbody[j]);if(/^\s/.test(elem))div.insertBefore(context.createTextNode(elem.match(/^\s*/)[0]),div.firstChild);}elem=jQuery.makeArray(div.childNodes);}if(elem.length===0&&(!jQuery.nodeName(elem,"form")&&!jQuery.nodeName(elem,"select")))return;if(elem[0]==undefined||jQuery.nodeName(elem,"form")||elem.options)ret.push(elem);else
+ret=jQuery.merge(ret,elem);});return ret;},attr:function(elem,name,value){if(!elem||elem.nodeType==3||elem.nodeType==8)return undefined;var notxml=!jQuery.isXMLDoc(elem),set=value!==undefined,msie=jQuery.browser.msie;name=notxml&&jQuery.props[name]||name;if(elem.tagName){var special=/href|src|style/.test(name);if(name=="selected"&&jQuery.browser.safari)elem.parentNode.selectedIndex;if(name in elem&&notxml&&!special){if(set){if(name=="type"&&jQuery.nodeName(elem,"input")&&elem.parentNode)throw"type property can't be changed";elem[name]=value;}if(jQuery.nodeName(elem,"form")&&elem.getAttributeNode(name))return elem.getAttributeNode(name).nodeValue;return elem[name];}if(msie&&notxml&&name=="style")return jQuery.attr(elem.style,"cssText",value);if(set)elem.setAttribute(name,""+value);var attr=msie&&notxml&&special?elem.getAttribute(name,2):elem.getAttribute(name);return attr===null?undefined:attr;}if(msie&&name=="opacity"){if(set){elem.zoom=1;elem.filter=(elem.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(value)+''=="NaN"?"":"alpha(opacity="+value*100+")");}return elem.filter&&elem.filter.indexOf("opacity=")>=0?(parseFloat(elem.filter.match(/opacity=([^)]*)/)[1])/100)+'':"";}name=name.replace(/-([a-z])/ig,function(all,letter){return letter.toUpperCase();});if(set)elem[name]=value;return elem[name];},trim:function(text){return(text||"").replace(/^\s+|\s+$/g,"");},makeArray:function(array){var ret=[];if(array!=null){var i=array.length;if(i==null||array.split||array.setInterval||array.call)ret[0]=array;else
+while(i)ret[--i]=array[i];}return ret;},inArray:function(elem,array){for(var i=0,length=array.length;i<length;i++)if(array[i]===elem)return i;return-1;},merge:function(first,second){var i=0,elem,pos=first.length;if(jQuery.browser.msie){while(elem=second[i++])if(elem.nodeType!=8)first[pos++]=elem;}else
+while(elem=second[i++])first[pos++]=elem;return first;},unique:function(array){var ret=[],done={};try{for(var i=0,length=array.length;i<length;i++){var id=jQuery.data(array[i]);if(!done[id]){done[id]=true;ret.push(array[i]);}}}catch(e){ret=array;}return ret;},grep:function(elems,callback,inv){var ret=[];for(var i=0,length=elems.length;i<length;i++)if(!inv!=!callback(elems[i],i))ret.push(elems[i]);return ret;},map:function(elems,callback){var ret=[];for(var i=0,length=elems.length;i<length;i++){var value=callback(elems[i],i);if(value!=null)ret[ret.length]=value;}return ret.concat.apply([],ret);}});var userAgent=navigator.userAgent.toLowerCase();jQuery.browser={version:(userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[])[1],safari:/webkit/.test(userAgent),opera:/opera/.test(userAgent),msie:/msie/.test(userAgent)&&!/opera/.test(userAgent),mozilla:/mozilla/.test(userAgent)&&!/(compatible|webkit)/.test(userAgent)};var styleFloat=jQuery.browser.msie?"styleFloat":"cssFloat";jQuery.extend({boxModel:!jQuery.browser.msie||document.compatMode=="CSS1Compat",props:{"for":"htmlFor","class":"className","float":styleFloat,cssFloat:styleFloat,styleFloat:styleFloat,readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing"}});jQuery.each({parent:function(elem){return elem.parentNode;},parents:function(elem){return jQuery.dir(elem,"parentNode");},next:function(elem){return jQuery.nth(elem,2,"nextSibling");},prev:function(elem){return jQuery.nth(elem,2,"previousSibling");},nextAll:function(elem){return jQuery.dir(elem,"nextSibling");},prevAll:function(elem){return jQuery.dir(elem,"previousSibling");},siblings:function(elem){return jQuery.sibling(elem.parentNode.firstChild,elem);},children:function(elem){return jQuery.sibling(elem.firstChild);},contents:function(elem){return jQuery.nodeName(elem,"iframe")?elem.contentDocument||elem.contentWindow.document:jQuery.makeArray(elem.childNodes);}},function(name,fn){jQuery.fn[name]=function(selector){var ret=jQuery.map(this,fn);if(selector&&typeof selector=="string")ret=jQuery.multiFilter(selector,ret);return this.pushStack(jQuery.unique(ret));};});jQuery.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(name,original){jQuery.fn[name]=function(){var args=arguments;return this.each(function(){for(var i=0,length=args.length;i<length;i++)jQuery(args[i])[original](this);});};});jQuery.each({removeAttr:function(name){jQuery.attr(this,name,"");if(this.nodeType==1)this.removeAttribute(name);},addClass:function(classNames){jQuery.className.add(this,classNames);},removeClass:function(classNames){jQuery.className.remove(this,classNames);},toggleClass:function(classNames){jQuery.className[jQuery.className.has(this,classNames)?"remove":"add"](this,classNames);},remove:function(selector){if(!selector||jQuery.filter(selector,[this]).r.length){jQuery("*",this).add(this).each(function(){jQuery.event.remove(this);jQuery.removeData(this);});if(this.parentNode)this.parentNode.removeChild(this);}},empty:function(){jQuery(">*",this).remove();while(this.firstChild)this.removeChild(this.firstChild);}},function(name,fn){jQuery.fn[name]=function(){return this.each(fn,arguments);};});jQuery.each(["Height","Width"],function(i,name){var type=name.toLowerCase();jQuery.fn[type]=function(size){return this[0]==window?jQuery.browser.opera&&document.body["client"+name]||jQuery.browser.safari&&window["inner"+name]||document.compatMode=="CSS1Compat"&&document.documentElement["client"+name]||document.body["client"+name]:this[0]==document?Math.max(Math.max(document.body["scroll"+name],document.documentElement["scroll"+name]),Math.max(document.body["offset"+name],document.documentElement["offset"+name])):size==undefined?(this.length?jQuery.css(this[0],type):null):this.css(type,size.constructor==String?size:size+"px");};});function num(elem,prop){return elem[0]&&parseInt(jQuery.curCSS(elem[0],prop,true),10)||0;}var chars=jQuery.browser.safari&&parseInt(jQuery.browser.version)<417?"(?:[\\w*_-]|\\\\.)":"(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",quickChild=new RegExp("^>\\s*("+chars+"+)"),quickID=new RegExp("^("+chars+"+)(#)("+chars+"+)"),quickClass=new RegExp("^([#.]?)("+chars+"*)");jQuery.extend({expr:{"":function(a,i,m){return m[2]=="*"||jQuery.nodeName(a,m[2]);},"#":function(a,i,m){return a.getAttribute("id")==m[2];},":":{lt:function(a,i,m){return i<m[3]-0;},gt:function(a,i,m){return i>m[3]-0;},nth:function(a,i,m){return m[3]-0==i;},eq:function(a,i,m){return m[3]-0==i;},first:function(a,i){return i==0;},last:function(a,i,m,r){return i==r.length-1;},even:function(a,i){return i%2==0;},odd:function(a,i){return i%2;},"first-child":function(a){return a.parentNode.getElementsByTagName("*")[0]==a;},"last-child":function(a){return jQuery.nth(a.parentNode.lastChild,1,"previousSibling")==a;},"only-child":function(a){return!jQuery.nth(a.parentNode.lastChild,2,"previousSibling");},parent:function(a){return a.firstChild;},empty:function(a){return!a.firstChild;},contains:function(a,i,m){return(a.textContent||a.innerText||jQuery(a).text()||"").indexOf(m[3])>=0;},visible:function(a){return"hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden";},hidden:function(a){return"hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden";},enabled:function(a){return!a.disabled;},disabled:function(a){return a.disabled;},checked:function(a){return a.checked;},selected:function(a){return a.selected||jQuery.attr(a,"selected");},text:function(a){return"text"==a.type;},radio:function(a){return"radio"==a.type;},checkbox:function(a){return"checkbox"==a.type;},file:function(a){return"file"==a.type;},password:function(a){return"password"==a.type;},submit:function(a){return"submit"==a.type;},image:function(a){return"image"==a.type;},reset:function(a){return"reset"==a.type;},button:function(a){return"button"==a.type||jQuery.nodeName(a,"button");},input:function(a){return/input|select|textarea|button/i.test(a.nodeName);},has:function(a,i,m){return jQuery.find(m[3],a).length;},header:function(a){return/h\d/i.test(a.nodeName);},animated:function(a){return jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length;}}},parse:[/^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,/^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,new RegExp("^([:.#]*)("+chars+"+)")],multiFilter:function(expr,elems,not){var old,cur=[];while(expr&&expr!=old){old=expr;var f=jQuery.filter(expr,elems,not);expr=f.t.replace(/^\s*,\s*/,"");cur=not?elems=f.r:jQuery.merge(cur,f.r);}return cur;},find:function(t,context){if(typeof t!="string")return[t];if(context&&context.nodeType!=1&&context.nodeType!=9)return[];context=context||document;var ret=[context],done=[],last,nodeName;while(t&&last!=t){var r=[];last=t;t=jQuery.trim(t);var foundToken=false,re=quickChild,m=re.exec(t);if(m){nodeName=m[1].toUpperCase();for(var i=0;ret[i];i++)for(var c=ret[i].firstChild;c;c=c.nextSibling)if(c.nodeType==1&&(nodeName=="*"||c.nodeName.toUpperCase()==nodeName))r.push(c);ret=r;t=t.replace(re,"");if(t.indexOf(" ")==0)continue;foundToken=true;}else{re=/^([>+~])\s*(\w*)/i;if((m=re.exec(t))!=null){r=[];var merge={};nodeName=m[2].toUpperCase();m=m[1];for(var j=0,rl=ret.length;j<rl;j++){var n=m=="~"||m=="+"?ret[j].nextSibling:ret[j].firstChild;for(;n;n=n.nextSibling)if(n.nodeType==1){var id=jQuery.data(n);if(m=="~"&&merge[id])break;if(!nodeName||n.nodeName.toUpperCase()==nodeName){if(m=="~")merge[id]=true;r.push(n);}if(m=="+")break;}}ret=r;t=jQuery.trim(t.replace(re,""));foundToken=true;}}if(t&&!foundToken){if(!t.indexOf(",")){if(context==ret[0])ret.shift();done=jQuery.merge(done,ret);r=ret=[context];t=" "+t.substr(1,t.length);}else{var re2=quickID;var m=re2.exec(t);if(m){m=[0,m[2],m[3],m[1]];}else{re2=quickClass;m=re2.exec(t);}m[2]=m[2].replace(/\\/g,"");var elem=ret[ret.length-1];if(m[1]=="#"&&elem&&elem.getElementById&&!jQuery.isXMLDoc(elem)){var oid=elem.getElementById(m[2]);if((jQuery.browser.msie||jQuery.browser.opera)&&oid&&typeof oid.id=="string"&&oid.id!=m[2])oid=jQuery('[@id="'+m[2]+'"]',elem)[0];ret=r=oid&&(!m[3]||jQuery.nodeName(oid,m[3]))?[oid]:[];}else{for(var i=0;ret[i];i++){var tag=m[1]=="#"&&m[3]?m[3]:m[1]!=""||m[0]==""?"*":m[2];if(tag=="*"&&ret[i].nodeName.toLowerCase()=="object")tag="param";r=jQuery.merge(r,ret[i].getElementsByTagName(tag));}if(m[1]==".")r=jQuery.classFilter(r,m[2]);if(m[1]=="#"){var tmp=[];for(var i=0;r[i];i++)if(r[i].getAttribute("id")==m[2]){tmp=[r[i]];break;}r=tmp;}ret=r;}t=t.replace(re2,"");}}if(t){var val=jQuery.filter(t,r);ret=r=val.r;t=jQuery.trim(val.t);}}if(t)ret=[];if(ret&&context==ret[0])ret.shift();done=jQuery.merge(done,ret);return done;},classFilter:function(r,m,not){m=" "+m+" ";var tmp=[];for(var i=0;r[i];i++){var pass=(" "+r[i].className+" ").indexOf(m)>=0;if(!not&&pass||not&&!pass)tmp.push(r[i]);}return tmp;},filter:function(t,r,not){var last;while(t&&t!=last){last=t;var p=jQuery.parse,m;for(var i=0;p[i];i++){m=p[i].exec(t);if(m){t=t.substring(m[0].length);m[2]=m[2].replace(/\\/g,"");break;}}if(!m)break;if(m[1]==":"&&m[2]=="not")r=isSimple.test(m[3])?jQuery.filter(m[3],r,true).r:jQuery(r).not(m[3]);else if(m[1]==".")r=jQuery.classFilter(r,m[2],not);else if(m[1]=="["){var tmp=[],type=m[3];for(var i=0,rl=r.length;i<rl;i++){var a=r[i],z=a[jQuery.props[m[2]]||m[2]];if(z==null||/href|src|selected/.test(m[2]))z=jQuery.attr(a,m[2])||'';if((type==""&&!!z||type=="="&&z==m[5]||type=="!="&&z!=m[5]||type=="^="&&z&&!z.indexOf(m[5])||type=="$="&&z.substr(z.length-m[5].length)==m[5]||(type=="*="||type=="~=")&&z.indexOf(m[5])>=0)^not)tmp.push(a);}r=tmp;}else if(m[1]==":"&&m[2]=="nth-child"){var merge={},tmp=[],test=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(m[3]=="even"&&"2n"||m[3]=="odd"&&"2n+1"||!/\D/.test(m[3])&&"0n+"+m[3]||m[3]),first=(test[1]+(test[2]||1))-0,last=test[3]-0;for(var i=0,rl=r.length;i<rl;i++){var node=r[i],parentNode=node.parentNode,id=jQuery.data(parentNode);if(!merge[id]){var c=1;for(var n=parentNode.firstChild;n;n=n.nextSibling)if(n.nodeType==1)n.nodeIndex=c++;merge[id]=true;}var add=false;if(first==0){if(node.nodeIndex==last)add=true;}else if((node.nodeIndex-last)%first==0&&(node.nodeIndex-last)/first>=0)add=true;if(add^not)tmp.push(node);}r=tmp;}else{var fn=jQuery.expr[m[1]];if(typeof fn=="object")fn=fn[m[2]];if(typeof fn=="string")fn=eval("false||function(a,i){return "+fn+";}");r=jQuery.grep(r,function(elem,i){return fn(elem,i,m,r);},not);}}return{r:r,t:t};},dir:function(elem,dir){var matched=[],cur=elem[dir];while(cur&&cur!=document){if(cur.nodeType==1)matched.push(cur);cur=cur[dir];}return matched;},nth:function(cur,result,dir,elem){result=result||1;var num=0;for(;cur;cur=cur[dir])if(cur.nodeType==1&&++num==result)break;return cur;},sibling:function(n,elem){var r=[];for(;n;n=n.nextSibling){if(n.nodeType==1&&n!=elem)r.push(n);}return r;}});jQuery.event={add:function(elem,types,handler,data){if(elem.nodeType==3||elem.nodeType==8)return;if(jQuery.browser.msie&&elem.setInterval)elem=window;if(!handler.guid)handler.guid=this.guid++;if(data!=undefined){var fn=handler;handler=this.proxy(fn,function(){return fn.apply(this,arguments);});handler.data=data;}var events=jQuery.data(elem,"events")||jQuery.data(elem,"events",{}),handle=jQuery.data(elem,"handle")||jQuery.data(elem,"handle",function(){if(typeof jQuery!="undefined"&&!jQuery.event.triggered)return jQuery.event.handle.apply(arguments.callee.elem,arguments);});handle.elem=elem;jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];handler.type=parts[1];var handlers=events[type];if(!handlers){handlers=events[type]={};if(!jQuery.event.special[type]||jQuery.event.special[type].setup.call(elem)===false){if(elem.addEventListener)elem.addEventListener(type,handle,false);else if(elem.attachEvent)elem.attachEvent("on"+type,handle);}}handlers[handler.guid]=handler;jQuery.event.global[type]=true;});elem=null;},guid:1,global:{},remove:function(elem,types,handler){if(elem.nodeType==3||elem.nodeType==8)return;var events=jQuery.data(elem,"events"),ret,index;if(events){if(types==undefined||(typeof types=="string"&&types.charAt(0)=="."))for(var type in events)this.remove(elem,type+(types||""));else{if(types.type){handler=types.handler;types=types.type;}jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];if(events[type]){if(handler)delete events[type][handler.guid];else
+for(handler in events[type])if(!parts[1]||events[type][handler].type==parts[1])delete events[type][handler];for(ret in events[type])break;if(!ret){if(!jQuery.event.special[type]||jQuery.event.special[type].teardown.call(elem)===false){if(elem.removeEventListener)elem.removeEventListener(type,jQuery.data(elem,"handle"),false);else if(elem.detachEvent)elem.detachEvent("on"+type,jQuery.data(elem,"handle"));}ret=null;delete events[type];}}});}for(ret in events)break;if(!ret){var handle=jQuery.data(elem,"handle");if(handle)handle.elem=null;jQuery.removeData(elem,"events");jQuery.removeData(elem,"handle");}}},trigger:function(type,data,elem,donative,extra){data=jQuery.makeArray(data);if(type.indexOf("!")>=0){type=type.slice(0,-1);var exclusive=true;}if(!elem){if(this.global[type])jQuery("*").add([window,document]).trigger(type,data);}else{if(elem.nodeType==3||elem.nodeType==8)return undefined;var val,ret,fn=jQuery.isFunction(elem[type]||null),event=!data[0]||!data[0].preventDefault;if(event){data.unshift({type:type,target:elem,preventDefault:function(){},stopPropagation:function(){},timeStamp:now()});data[0][expando]=true;}data[0].type=type;if(exclusive)data[0].exclusive=true;var handle=jQuery.data(elem,"handle");if(handle)val=handle.apply(elem,data);if((!fn||(jQuery.nodeName(elem,'a')&&type=="click"))&&elem["on"+type]&&elem["on"+type].apply(elem,data)===false)val=false;if(event)data.shift();if(extra&&jQuery.isFunction(extra)){ret=extra.apply(elem,val==null?data:data.concat(val));if(ret!==undefined)val=ret;}if(fn&&donative!==false&&val!==false&&!(jQuery.nodeName(elem,'a')&&type=="click")){this.triggered=true;try{elem[type]();}catch(e){}}this.triggered=false;}return val;},handle:function(event){var val,ret,namespace,all,handlers;event=arguments[0]=jQuery.event.fix(event||window.event);namespace=event.type.split(".");event.type=namespace[0];namespace=namespace[1];all=!namespace&&!event.exclusive;handlers=(jQuery.data(this,"events")||{})[event.type];for(var j in handlers){var handler=handlers[j];if(all||handler.type==namespace){event.handler=handler;event.data=handler.data;ret=handler.apply(this,arguments);if(val!==false)val=ret;if(ret===false){event.preventDefault();event.stopPropagation();}}}return val;},fix:function(event){if(event[expando]==true)return event;var originalEvent=event;event={originalEvent:originalEvent};var props="altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target timeStamp toElement type view wheelDelta which".split(" ");for(var i=props.length;i;i--)event[props[i]]=originalEvent[props[i]];event[expando]=true;event.preventDefault=function(){if(originalEvent.preventDefault)originalEvent.preventDefault();originalEvent.returnValue=false;};event.stopPropagation=function(){if(originalEvent.stopPropagation)originalEvent.stopPropagation();originalEvent.cancelBubble=true;};event.timeStamp=event.timeStamp||now();if(!event.target)event.target=event.srcElement||document;if(event.target.nodeType==3)event.target=event.target.parentNode;if(!event.relatedTarget&&event.fromElement)event.relatedTarget=event.fromElement==event.target?event.toElement:event.fromElement;if(event.pageX==null&&event.clientX!=null){var doc=document.documentElement,body=document.body;event.pageX=event.clientX+(doc&&doc.scrollLeft||body&&body.scrollLeft||0)-(doc.clientLeft||0);event.pageY=event.clientY+(doc&&doc.scrollTop||body&&body.scrollTop||0)-(doc.clientTop||0);}if(!event.which&&((event.charCode||event.charCode===0)?event.charCode:event.keyCode))event.which=event.charCode||event.keyCode;if(!event.metaKey&&event.ctrlKey)event.metaKey=event.ctrlKey;if(!event.which&&event.button)event.which=(event.button&1?1:(event.button&2?3:(event.button&4?2:0)));return event;},proxy:function(fn,proxy){proxy.guid=fn.guid=fn.guid||proxy.guid||this.guid++;return proxy;},special:{ready:{setup:function(){bindReady();return;},teardown:function(){return;}},mouseenter:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseover",jQuery.event.special.mouseenter.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseover",jQuery.event.special.mouseenter.handler);return true;},handler:function(event){if(withinElement(event,this))return true;event.type="mouseenter";return jQuery.event.handle.apply(this,arguments);}},mouseleave:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseout",jQuery.event.special.mouseleave.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseout",jQuery.event.special.mouseleave.handler);return true;},handler:function(event){if(withinElement(event,this))return true;event.type="mouseleave";return jQuery.event.handle.apply(this,arguments);}}}};jQuery.fn.extend({bind:function(type,data,fn){return type=="unload"?this.one(type,data,fn):this.each(function(){jQuery.event.add(this,type,fn||data,fn&&data);});},one:function(type,data,fn){var one=jQuery.event.proxy(fn||data,function(event){jQuery(this).unbind(event,one);return(fn||data).apply(this,arguments);});return this.each(function(){jQuery.event.add(this,type,one,fn&&data);});},unbind:function(type,fn){return this.each(function(){jQuery.event.remove(this,type,fn);});},trigger:function(type,data,fn){return this.each(function(){jQuery.event.trigger(type,data,this,true,fn);});},triggerHandler:function(type,data,fn){return this[0]&&jQuery.event.trigger(type,data,this[0],false,fn);},toggle:function(fn){var args=arguments,i=1;while(i<args.length)jQuery.event.proxy(fn,args[i++]);return this.click(jQuery.event.proxy(fn,function(event){this.lastToggle=(this.lastToggle||0)%i;event.preventDefault();return args[this.lastToggle++].apply(this,arguments)||false;}));},hover:function(fnOver,fnOut){return this.bind('mouseenter',fnOver).bind('mouseleave',fnOut);},ready:function(fn){bindReady();if(jQuery.isReady)fn.call(document,jQuery);else
+jQuery.readyList.push(function(){return fn.call(this,jQuery);});return this;}});jQuery.extend({isReady:false,readyList:[],ready:function(){if(!jQuery.isReady){jQuery.isReady=true;if(jQuery.readyList){jQuery.each(jQuery.readyList,function(){this.call(document);});jQuery.readyList=null;}jQuery(document).triggerHandler("ready");}}});var readyBound=false;function bindReady(){if(readyBound)return;readyBound=true;if(document.addEventListener&&!jQuery.browser.opera)document.addEventListener("DOMContentLoaded",jQuery.ready,false);if(jQuery.browser.msie&&window==top)(function(){if(jQuery.isReady)return;try{document.documentElement.doScroll("left");}catch(error){setTimeout(arguments.callee,0);return;}jQuery.ready();})();if(jQuery.browser.opera)document.addEventListener("DOMContentLoaded",function(){if(jQuery.isReady)return;for(var i=0;i<document.styleSheets.length;i++)if(document.styleSheets[i].disabled){setTimeout(arguments.callee,0);return;}jQuery.ready();},false);if(jQuery.browser.safari){var numStyles;(function(){if(jQuery.isReady)return;if(document.readyState!="loaded"&&document.readyState!="complete"){setTimeout(arguments.callee,0);return;}if(numStyles===undefined)numStyles=jQuery("style, link[rel=stylesheet]").length;if(document.styleSheets.length!=numStyles){setTimeout(arguments.callee,0);return;}jQuery.ready();})();}jQuery.event.add(window,"load",jQuery.ready);}jQuery.each(("blur,focus,load,resize,scroll,unload,click,dblclick,"+"mousedown,mouseup,mousemove,mouseover,mouseout,change,select,"+"submit,keydown,keypress,keyup,error").split(","),function(i,name){jQuery.fn[name]=function(fn){return fn?this.bind(name,fn):this.trigger(name);};});var withinElement=function(event,elem){var parent=event.relatedTarget;while(parent&&parent!=elem)try{parent=parent.parentNode;}catch(error){parent=elem;}return parent==elem;};jQuery(window).bind("unload",function(){jQuery("*").add(document).unbind();});jQuery.fn.extend({_load:jQuery.fn.load,load:function(url,params,callback){if(typeof url!='string')return this._load(url);var off=url.indexOf(" ");if(off>=0){var selector=url.slice(off,url.length);url=url.slice(0,off);}callback=callback||function(){};var type="GET";if(params)if(jQuery.isFunction(params)){callback=params;params=null;}else{params=jQuery.param(params);type="POST";}var self=this;jQuery.ajax({url:url,type:type,dataType:"html",data:params,complete:function(res,status){if(status=="success"||status=="notmodified")self.html(selector?jQuery("<div/>").append(res.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(selector):res.responseText);self.each(callback,[res.responseText,status,res]);}});return this;},serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){return jQuery.nodeName(this,"form")?jQuery.makeArray(this.elements):this;}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type));}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:val.constructor==Array?jQuery.map(val,function(val,i){return{name:elem.name,value:val};}):{name:elem.name,value:val};}).get();}});jQuery.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(i,o){jQuery.fn[o]=function(f){return this.bind(o,f);};});var jsc=now();jQuery.extend({get:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data=null;}return jQuery.ajax({type:"GET",url:url,data:data,success:callback,dataType:type});},getScript:function(url,callback){return jQuery.get(url,null,callback,"script");},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json");},post:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data={};}return jQuery.ajax({type:"POST",url:url,data:data,success:callback,dataType:type});},ajaxSetup:function(settings){jQuery.extend(jQuery.ajaxSettings,settings);},ajaxSettings:{url:location.href,global:true,type:"GET",timeout:0,contentType:"application/x-www-form-urlencoded",processData:true,async:true,data:null,username:null,password:null,accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(s){s=jQuery.extend(true,s,jQuery.extend(true,{},jQuery.ajaxSettings,s));var jsonp,jsre=/=\?(&|$)/g,status,data,type=s.type.toUpperCase();if(s.data&&s.processData&&typeof s.data!="string")s.data=jQuery.param(s.data);if(s.dataType=="jsonp"){if(type=="GET"){if(!s.url.match(jsre))s.url+=(s.url.match(/\?/)?"&":"?")+(s.jsonp||"callback")+"=?";}else if(!s.data||!s.data.match(jsre))s.data=(s.data?s.data+"&":"")+(s.jsonp||"callback")+"=?";s.dataType="json";}if(s.dataType=="json"&&(s.data&&s.data.match(jsre)||s.url.match(jsre))){jsonp="jsonp"+jsc++;if(s.data)s.data=(s.data+"").replace(jsre,"="+jsonp+"$1");s.url=s.url.replace(jsre,"="+jsonp+"$1");s.dataType="script";window[jsonp]=function(tmp){data=tmp;success();complete();window[jsonp]=undefined;try{delete window[jsonp];}catch(e){}if(head)head.removeChild(script);};}if(s.dataType=="script"&&s.cache==null)s.cache=false;if(s.cache===false&&type=="GET"){var ts=now();var ret=s.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+ts+"$2");s.url=ret+((ret==s.url)?(s.url.match(/\?/)?"&":"?")+"_="+ts:"");}if(s.data&&type=="GET"){s.url+=(s.url.match(/\?/)?"&":"?")+s.data;s.data=null;}if(s.global&&!jQuery.active++)jQuery.event.trigger("ajaxStart");var remote=/^(?:\w+:)?\/\/([^\/?#]+)/;if(s.dataType=="script"&&type=="GET"&&remote.test(s.url)&&remote.exec(s.url)[1]!=location.host){var head=document.getElementsByTagName("head")[0];var script=document.createElement("script");script.src=s.url;if(s.scriptCharset)script.charset=s.scriptCharset;if(!jsonp){var done=false;script.onload=script.onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){done=true;success();complete();head.removeChild(script);}};}head.appendChild(script);return undefined;}var requestDone=false;var xhr=window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest();if(s.username)xhr.open(type,s.url,s.async,s.username,s.password);else
+xhr.open(type,s.url,s.async);try{if(s.data)xhr.setRequestHeader("Content-Type",s.contentType);if(s.ifModified)xhr.setRequestHeader("If-Modified-Since",jQuery.lastModified[s.url]||"Thu, 01 Jan 1970 00:00:00 GMT");xhr.setRequestHeader("X-Requested-With","XMLHttpRequest");xhr.setRequestHeader("Accept",s.dataType&&s.accepts[s.dataType]?s.accepts[s.dataType]+", */*":s.accepts._default);}catch(e){}if(s.beforeSend&&s.beforeSend(xhr,s)===false){s.global&&jQuery.active--;xhr.abort();return false;}if(s.global)jQuery.event.trigger("ajaxSend",[xhr,s]);var onreadystatechange=function(isTimeout){if(!requestDone&&xhr&&(xhr.readyState==4||isTimeout=="timeout")){requestDone=true;if(ival){clearInterval(ival);ival=null;}status=isTimeout=="timeout"&&"timeout"||!jQuery.httpSuccess(xhr)&&"error"||s.ifModified&&jQuery.httpNotModified(xhr,s.url)&&"notmodified"||"success";if(status=="success"){try{data=jQuery.httpData(xhr,s.dataType,s.dataFilter);}catch(e){status="parsererror";}}if(status=="success"){var modRes;try{modRes=xhr.getResponseHeader("Last-Modified");}catch(e){}if(s.ifModified&&modRes)jQuery.lastModified[s.url]=modRes;if(!jsonp)success();}else
+jQuery.handleError(s,xhr,status);complete();if(s.async)xhr=null;}};if(s.async){var ival=setInterval(onreadystatechange,13);if(s.timeout>0)setTimeout(function(){if(xhr){xhr.abort();if(!requestDone)onreadystatechange("timeout");}},s.timeout);}try{xhr.send(s.data);}catch(e){jQuery.handleError(s,xhr,null,e);}if(!s.async)onreadystatechange();function success(){if(s.success)s.success(data,status);if(s.global)jQuery.event.trigger("ajaxSuccess",[xhr,s]);}function complete(){if(s.complete)s.complete(xhr,status);if(s.global)jQuery.event.trigger("ajaxComplete",[xhr,s]);if(s.global&&!--jQuery.active)jQuery.event.trigger("ajaxStop");}return xhr;},handleError:function(s,xhr,status,e){if(s.error)s.error(xhr,status,e);if(s.global)jQuery.event.trigger("ajaxError",[xhr,s,e]);},active:0,httpSuccess:function(xhr){try{return!xhr.status&&location.protocol=="file:"||(xhr.status>=200&&xhr.status<300)||xhr.status==304||xhr.status==1223||jQuery.browser.safari&&xhr.status==undefined;}catch(e){}return false;},httpNotModified:function(xhr,url){try{var xhrRes=xhr.getResponseHeader("Last-Modified");return xhr.status==304||xhrRes==jQuery.lastModified[url]||jQuery.browser.safari&&xhr.status==undefined;}catch(e){}return false;},httpData:function(xhr,type,filter){var ct=xhr.getResponseHeader("content-type"),xml=type=="xml"||!type&&ct&&ct.indexOf("xml")>=0,data=xml?xhr.responseXML:xhr.responseText;if(xml&&data.documentElement.tagName=="parsererror")throw"parsererror";if(filter)data=filter(data,type);if(type=="script")jQuery.globalEval(data);if(type=="json")data=eval("("+data+")");return data;},param:function(a){var s=[];if(a.constructor==Array||a.jquery)jQuery.each(a,function(){s.push(encodeURIComponent(this.name)+"="+encodeURIComponent(this.value));});else
+for(var j in a)if(a[j]&&a[j].constructor==Array)jQuery.each(a[j],function(){s.push(encodeURIComponent(j)+"="+encodeURIComponent(this));});else
+s.push(encodeURIComponent(j)+"="+encodeURIComponent(jQuery.isFunction(a[j])?a[j]():a[j]));return s.join("&").replace(/%20/g,"+");}});jQuery.fn.extend({show:function(speed,callback){return speed?this.animate({height:"show",width:"show",opacity:"show"},speed,callback):this.filter(":hidden").each(function(){this.style.display=this.oldblock||"";if(jQuery.css(this,"display")=="none"){var elem=jQuery("<"+this.tagName+" />").appendTo("body");this.style.display=elem.css("display");if(this.style.display=="none")this.style.display="block";elem.remove();}}).end();},hide:function(speed,callback){return speed?this.animate({height:"hide",width:"hide",opacity:"hide"},speed,callback):this.filter(":visible").each(function(){this.oldblock=this.oldblock||jQuery.css(this,"display");this.style.display="none";}).end();},_toggle:jQuery.fn.toggle,toggle:function(fn,fn2){return jQuery.isFunction(fn)&&jQuery.isFunction(fn2)?this._toggle.apply(this,arguments):fn?this.animate({height:"toggle",width:"toggle",opacity:"toggle"},fn,fn2):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();});},slideDown:function(speed,callback){return this.animate({height:"show"},speed,callback);},slideUp:function(speed,callback){return this.animate({height:"hide"},speed,callback);},slideToggle:function(speed,callback){return this.animate({height:"toggle"},speed,callback);},fadeIn:function(speed,callback){return this.animate({opacity:"show"},speed,callback);},fadeOut:function(speed,callback){return this.animate({opacity:"hide"},speed,callback);},fadeTo:function(speed,to,callback){return this.animate({opacity:to},speed,callback);},animate:function(prop,speed,easing,callback){var optall=jQuery.speed(speed,easing,callback);return this[optall.queue===false?"each":"queue"](function(){if(this.nodeType!=1)return false;var opt=jQuery.extend({},optall),p,hidden=jQuery(this).is(":hidden"),self=this;for(p in prop){if(prop[p]=="hide"&&hidden||prop[p]=="show"&&!hidden)return opt.complete.call(this);if(p=="height"||p=="width"){opt.display=jQuery.css(this,"display");opt.overflow=this.style.overflow;}}if(opt.overflow!=null)this.style.overflow="hidden";opt.curAnim=jQuery.extend({},prop);jQuery.each(prop,function(name,val){var e=new jQuery.fx(self,opt,name);if(/toggle|show|hide/.test(val))e[val=="toggle"?hidden?"show":"hide":val](prop);else{var parts=val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),start=e.cur(true)||0;if(parts){var end=parseFloat(parts[2]),unit=parts[3]||"px";if(unit!="px"){self.style[name]=(end||1)+unit;start=((end||1)/e.cur(true))*start;self.style[name]=start+unit;}if(parts[1])end=((parts[1]=="-="?-1:1)*end)+start;e.custom(start,end,unit);}else
+e.custom(start,val,"");}});return true;});},queue:function(type,fn){if(jQuery.isFunction(type)||(type&&type.constructor==Array)){fn=type;type="fx";}if(!type||(typeof type=="string"&&!fn))return queue(this[0],type);return this.each(function(){if(fn.constructor==Array)queue(this,type,fn);else{queue(this,type).push(fn);if(queue(this,type).length==1)fn.call(this);}});},stop:function(clearQueue,gotoEnd){var timers=jQuery.timers;if(clearQueue)this.queue([]);this.each(function(){for(var i=timers.length-1;i>=0;i--)if(timers[i].elem==this){if(gotoEnd)timers[i](true);timers.splice(i,1);}});if(!gotoEnd)this.dequeue();return this;}});var queue=function(elem,type,array){if(elem){type=type||"fx";var q=jQuery.data(elem,type+"queue");if(!q||array)q=jQuery.data(elem,type+"queue",jQuery.makeArray(array));}return q;};jQuery.fn.dequeue=function(type){type=type||"fx";return this.each(function(){var q=queue(this,type);q.shift();if(q.length)q[0].call(this);});};jQuery.extend({speed:function(speed,easing,fn){var opt=speed&&speed.constructor==Object?speed:{complete:fn||!fn&&easing||jQuery.isFunction(speed)&&speed,duration:speed,easing:fn&&easing||easing&&easing.constructor!=Function&&easing};opt.duration=(opt.duration&&opt.duration.constructor==Number?opt.duration:jQuery.fx.speeds[opt.duration])||jQuery.fx.speeds.def;opt.old=opt.complete;opt.complete=function(){if(opt.queue!==false)jQuery(this).dequeue();if(jQuery.isFunction(opt.old))opt.old.call(this);};return opt;},easing:{linear:function(p,n,firstNum,diff){return firstNum+diff*p;},swing:function(p,n,firstNum,diff){return((-Math.cos(p*Math.PI)/2)+0.5)*diff+firstNum;}},timers:[],timerId:null,fx:function(elem,options,prop){this.options=options;this.elem=elem;this.prop=prop;if(!options.orig)options.orig={};}});jQuery.fx.prototype={update:function(){if(this.options.step)this.options.step.call(this.elem,this.now,this);(jQuery.fx.step[this.prop]||jQuery.fx.step._default)(this);if(this.prop=="height"||this.prop=="width")this.elem.style.display="block";},cur:function(force){if(this.elem[this.prop]!=null&&this.elem.style[this.prop]==null)return this.elem[this.prop];var r=parseFloat(jQuery.css(this.elem,this.prop,force));return r&&r>-10000?r:parseFloat(jQuery.curCSS(this.elem,this.prop))||0;},custom:function(from,to,unit){this.startTime=now();this.start=from;this.end=to;this.unit=unit||this.unit||"px";this.now=this.start;this.pos=this.state=0;this.update();var self=this;function t(gotoEnd){return self.step(gotoEnd);}t.elem=this.elem;jQuery.timers.push(t);if(jQuery.timerId==null){jQuery.timerId=setInterval(function(){var timers=jQuery.timers;for(var i=0;i<timers.length;i++)if(!timers[i]())timers.splice(i--,1);if(!timers.length){clearInterval(jQuery.timerId);jQuery.timerId=null;}},13);}},show:function(){this.options.orig[this.prop]=jQuery.attr(this.elem.style,this.prop);this.options.show=true;this.custom(0,this.cur());if(this.prop=="width"||this.prop=="height")this.elem.style[this.prop]="1px";jQuery(this.elem).show();},hide:function(){this.options.orig[this.prop]=jQuery.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0);},step:function(gotoEnd){var t=now();if(gotoEnd||t>this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var done=true;for(var i in this.options.curAnim)if(this.options.curAnim[i]!==true)done=false;if(done){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(jQuery.css(this.elem,"display")=="none")this.elem.style.display="block";}if(this.options.hide)this.elem.style.display="none";if(this.options.hide||this.options.show)for(var p in this.options.curAnim)jQuery.attr(this.elem.style,p,this.options.orig[p]);}if(done)this.options.complete.call(this.elem);return false;}else{var n=t-this.startTime;this.state=n/this.options.duration;this.pos=jQuery.easing[this.options.easing||(jQuery.easing.swing?"swing":"linear")](this.state,n,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update();}return true;}};jQuery.extend(jQuery.fx,{speeds:{slow:600,fast:200,def:400},step:{scrollLeft:function(fx){fx.elem.scrollLeft=fx.now;},scrollTop:function(fx){fx.elem.scrollTop=fx.now;},opacity:function(fx){jQuery.attr(fx.elem.style,"opacity",fx.now);},_default:function(fx){fx.elem.style[fx.prop]=fx.now+fx.unit;}}});jQuery.fn.offset=function(){var left=0,top=0,elem=this[0],results;if(elem)with(jQuery.browser){var parent=elem.parentNode,offsetChild=elem,offsetParent=elem.offsetParent,doc=elem.ownerDocument,safari2=safari&&parseInt(version)<522&&!/adobeair/i.test(userAgent),css=jQuery.curCSS,fixed=css(elem,"position")=="fixed";if(elem.getBoundingClientRect){var box=elem.getBoundingClientRect();add(box.left+Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),box.top+Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));add(-doc.documentElement.clientLeft,-doc.documentElement.clientTop);}else{add(elem.offsetLeft,elem.offsetTop);while(offsetParent){add(offsetParent.offsetLeft,offsetParent.offsetTop);if(mozilla&&!/^t(able|d|h)$/i.test(offsetParent.tagName)||safari&&!safari2)border(offsetParent);if(!fixed&&css(offsetParent,"position")=="fixed")fixed=true;offsetChild=/^body$/i.test(offsetParent.tagName)?offsetChild:offsetParent;offsetParent=offsetParent.offsetParent;}while(parent&&parent.tagName&&!/^body|html$/i.test(parent.tagName)){if(!/^inline|table.*$/i.test(css(parent,"display")))add(-parent.scrollLeft,-parent.scrollTop);if(mozilla&&css(parent,"overflow")!="visible")border(parent);parent=parent.parentNode;}if((safari2&&(fixed||css(offsetChild,"position")=="absolute"))||(mozilla&&css(offsetChild,"position")!="absolute"))add(-doc.body.offsetLeft,-doc.body.offsetTop);if(fixed)add(Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));}results={top:top,left:left};}function border(elem){add(jQuery.curCSS(elem,"borderLeftWidth",true),jQuery.curCSS(elem,"borderTopWidth",true));}function add(l,t){left+=parseInt(l,10)||0;top+=parseInt(t,10)||0;}return results;};jQuery.fn.extend({position:function(){var left=0,top=0,results;if(this[0]){var offsetParent=this.offsetParent(),offset=this.offset(),parentOffset=/^body|html$/i.test(offsetParent[0].tagName)?{top:0,left:0}:offsetParent.offset();offset.top-=num(this,'marginTop');offset.left-=num(this,'marginLeft');parentOffset.top+=num(offsetParent,'borderTopWidth');parentOffset.left+=num(offsetParent,'borderLeftWidth');results={top:offset.top-parentOffset.top,left:offset.left-parentOffset.left};}return results;},offsetParent:function(){var offsetParent=this[0].offsetParent;while(offsetParent&&(!/^body|html$/i.test(offsetParent.tagName)&&jQuery.css(offsetParent,'position')=='static'))offsetParent=offsetParent.offsetParent;return jQuery(offsetParent);}});jQuery.each(['Left','Top'],function(i,name){var method='scroll'+name;jQuery.fn[method]=function(val){if(!this[0])return;return val!=undefined?this.each(function(){this==window||this==document?window.scrollTo(!i?val:jQuery(window).scrollLeft(),i?val:jQuery(window).scrollTop()):this[method]=val;}):this[0]==window||this[0]==document?self[i?'pageYOffset':'pageXOffset']||jQuery.boxModel&&document.documentElement[method]||document.body[method]:this[0][method];};});jQuery.each(["Height","Width"],function(i,name){var tl=i?"Left":"Top",br=i?"Right":"Bottom";jQuery.fn["inner"+name]=function(){return this[name.toLowerCase()]()+num(this,"padding"+tl)+num(this,"padding"+br);};jQuery.fn["outer"+name]=function(margin){return this["inner"+name]()+num(this,"border"+tl+"Width")+num(this,"border"+br+"Width")+(margin?num(this,"margin"+tl)+num(this,"margin"+br):0);};});})();
diff --git a/data/static/js/preview.js b/data/static/js/preview.js
new file mode 100644
--- /dev/null
+++ b/data/static/js/preview.js
@@ -0,0 +1,17 @@
+function updatePreviewPane() {
+    $("#previewpane").hide();
+    var url = location.pathname.replace(/_edit\//,"_preview/");
+    $("#previewpane").load(url, { "raw" : $("#editedText").attr("value")
+                                , "format" : $("#format").attr("value")
+                                , "lhs": $("#lhs").attr("value")
+                                , "toc": $("#toc").attr("value")
+                                , "title": $("#title").attr("value")
+                                , "categories": $("#categories").attr("value")
+                                });
+    jQuery.getScript("/js/MathMLinHTML.js", function() {convert()});
+    $("#previewpane").fadeIn(1000);
+};
+$(document).ready(function(){
+    $("#previewButton").show();
+  });
+
diff --git a/data/static/js/search.js b/data/static/js/search.js
new file mode 100644
--- /dev/null
+++ b/data/static/js/search.js
@@ -0,0 +1,13 @@
+function toggleMatches(obj) {
+  obj.next('.matches').slideToggle(300);
+  if (obj.html() == '[show matches]') {
+      obj.html('[hide matches]');
+    } else {
+      obj.html('[show matches]');
+    };
+  }
+$(function() {
+  $('a.showmatch').attr("onClick", "toggleMatches($(this));");
+  $('pre.matches').hide();
+  $('a.showmatch').show();
+  });
diff --git a/data/static/js/uploadForm.js b/data/static/js/uploadForm.js
new file mode 100644
--- /dev/null
+++ b/data/static/js/uploadForm.js
@@ -0,0 +1,3 @@
+$(document).ready(function(){
+    $("#file").change(function () { $("#wikiname").val($(this).val()); });
+  });
diff --git a/data/static/robots.txt b/data/static/robots.txt
new file mode 100644
--- /dev/null
+++ b/data/static/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /_
diff --git a/data/template.html b/data/template.html
deleted file mode 100644
--- a/data/template.html
+++ /dev/null
@@ -1,74 +0,0 @@
-<!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">
-  <head>
-    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
-    <title>Wiki - $pagetitle$</title>
-    $if(printable)$
-    <link href="/css/print.css" rel="stylesheet" media="all" type= "text/css" />
-    $else$
-    <link href="/css/screen.css" rel="stylesheet" media="screen, projection" type="text/css" />
-    <link href="/css/print.css" rel="stylesheet" media="print" type= "text/css" />
-    $endif$
-    <!--[if IE]><link href="/css/ie.css" rel="stylesheet" media="screen, projection" type="text/css" /><![endif]-->
-  </head>
-  <body>
-    <div id="container">
-      <div id="sidebar">
-        <div id="logo">
-          <a href="/" title="Go to top page"><img src="/img/logo.png" /></a>
-        </div>
-        <div class="sitenav">
-          <fieldset>
-            <legend>Site</legend>
-            <ul>
-              <li><a href="/">Front page</a></li>
-              <li><a href="/_index">All pages</a></li>
-              <li><a href="/_random">Random page</a></li>
-              <li><a href="/_activity">Recent activity</a></li>
-              <li><a href="/_upload">Upload a file</a></li>
-              <li><a href="/Help">Help</a></li>
-            </ul>
-            $searchbox$
-          </fieldset>
-        </div>
-        $if(pagetools)$
-        <div class="pageTools">
-          <fieldset>
-            <legend>This page</legend>
-            <ul>
-              <li><a href="/$pagename$?revision=$revision$&amp;showraw">Raw page source</a></li>
-              <li><a href="/$pagename$?revision=$revision$&amp;printable">Printable version</a></li>
-              <li><a href="/$pagename$?revision=$sha1$">Permanent link</a></li>
-              <li><a href="/$pagename$?delete">Delete this page</a></li>
-            </ul>
-            $exportbox$
-          </fieldset>
-        </div>
-        $endif$
-      </div>
-      <div id="maincol">
-        <div id="userbox">
-          $if(user)$
-          <a href="/_logout">Logout $user$</a>
-          $else$
-          <a href="/_login">Login</a> &bull; <a href="/_register">Get an account</a>
-          $endif$
-        </div>
-        $tabs$ 
-        <div id="content">
-          $if(nothead)$
-          <h2 class="revision">Revision $revision$</h2>
-          $endif$
-          <h1 class="pageTitle"><a href="/$pagename$">$pagetitle$</a></h1>
-          $messages$
-          <div id="wikipage" ondblclick="window.location='/$pagename$?edit">
-            $content$
-          </div>
-        </div>
-        <div id="footer">powered by <a href="http://github.com/jgm/gitit/tree/master/">gitit</a></div>
-      </div>
-    </div>
-    $javascripts$
-  </body>
-</html>
diff --git a/data/templates/content.st b/data/templates/content.st
new file mode 100644
--- /dev/null
+++ b/data/templates/content.st
@@ -0,0 +1,10 @@
+<div id="content">
+  $if(revision)$
+    <h2 class="revision">Revision $revision$</h2>
+  $endif$
+  <h1 class="pageTitle"><a href="$base$$pageUrl$">$pagetitle$</a></h1>
+  $if(messages)$
+    $messages()$
+  $endif$
+  $content$
+</div>
diff --git a/data/templates/expire.st b/data/templates/expire.st
new file mode 100644
--- /dev/null
+++ b/data/templates/expire.st
@@ -0,0 +1,11 @@
+$if(usecache)$
+<script type="text/javascript" src="$base$/js/jquery.hotkeys-0.7.9.min.js"></script>
+<script type="text/javascript">
+/* <![CDATA[ */
+     \$(document).bind("keydown", "ctrl+r", function() {
+         \$.post("$base$/_expire$pageUrl$");
+         return true;
+         });
+/* ]]> */
+</script>
+$endif$
diff --git a/data/templates/footer.st b/data/templates/footer.st
new file mode 100644
--- /dev/null
+++ b/data/templates/footer.st
@@ -0,0 +1,1 @@
+<div id="footer">powered by <a href="http://github.com/jgm/gitit/tree/master/">gitit</a></div>
diff --git a/data/templates/getuser.st b/data/templates/getuser.st
new file mode 100644
--- /dev/null
+++ b/data/templates/getuser.st
@@ -0,0 +1,14 @@
+<script type="text/javascript">
+/* <![CDATA[ */
+  \$.get("$base$/_user", {}, function(username, status) {
+     \$("#username").text(username);
+     if (username == "") {  // nobody logged in
+        \$("#logoutlink").hide();
+        \$("#loginlink").show();
+     } else {
+        \$("#logoutlink").show();
+        \$("#loginlink").hide();
+     };
+   });
+/* ]]> */
+</script>
diff --git a/data/templates/listitem.st b/data/templates/listitem.st
new file mode 100644
--- /dev/null
+++ b/data/templates/listitem.st
@@ -0,0 +1,1 @@
+<li>$it$</li>
diff --git a/data/templates/logo.st b/data/templates/logo.st
new file mode 100644
--- /dev/null
+++ b/data/templates/logo.st
@@ -0,0 +1,3 @@
+<div id="logo">
+  <a href="$base$/" title="Go to top page"><img src="$base$/img/logo.png" /></a>
+</div>
diff --git a/data/templates/markuphelp.st b/data/templates/markuphelp.st
new file mode 100644
--- /dev/null
+++ b/data/templates/markuphelp.st
@@ -0,0 +1,3 @@
+<div class="markupHelp">
+  $markuphelp$
+</div>
diff --git a/data/templates/messages.st b/data/templates/messages.st
new file mode 100644
--- /dev/null
+++ b/data/templates/messages.st
@@ -0,0 +1,3 @@
+<ul class="messages">
+  $messages:listitem()$
+</ul>
diff --git a/data/templates/page.st b/data/templates/page.st
new file mode 100644
--- /dev/null
+++ b/data/templates/page.st
@@ -0,0 +1,46 @@
+<!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">
+  <head>
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    $if(feed)$
+    <link href="$base$/_feed" type="application/atom+xml" rel="alternate" title="Sitewide ATOM Feed" />
+    <link href="$base$/_feed$pageUrl$" type="application/atom+xml" rel="alternate" title="This page's ATOM Feed" />
+    $endif$
+    <title>Wiki - $pagetitle$</title>
+    $if(printable)$
+    <link href="$base$/css/print.css" rel="stylesheet" media="all" type= "text/css" />
+    $else$
+    <link href="$base$/css/custom.css" rel="stylesheet" media="screen, projection" type="text/css" />
+    <link href="$base$/css/print.css" rel="stylesheet" media="print" type= "text/css" />
+    $endif$
+    <!--[if IE]><link href="$base$/css/ie.css" rel="stylesheet" media="screen, projection" type="text/css" /><![endif]-->
+  </head>
+  <body>
+    <div id="doc3" class="yui-t1">
+        <div id="yui-main">
+          <div id="maincol" class="yui-b">
+            $userbox()$
+            $tabs$ 
+            $content()$
+            $footer()$
+          </div>
+        </div>
+        <div id="sidebar" class="yui-b first">
+          $logo()$
+          $if(sitenav)$
+            $sitenav()$
+          $endif$
+          $if(pagetools)$
+            $pagetools()$
+          $endif$
+          $if(markuphelp)$
+            $markuphelp()$
+          $endif$
+        </div>
+    </div>
+    $javascripts$
+    $expire()$
+    $getuser()$
+  </body>
+</html>
diff --git a/data/templates/pagetools.st b/data/templates/pagetools.st
new file mode 100644
--- /dev/null
+++ b/data/templates/pagetools.st
@@ -0,0 +1,11 @@
+<div class="pageTools">
+  <fieldset>
+    <legend>This page</legend>
+    <ul>
+      <li><a href="$base$/_showraw$pageUrl$$if(revision)$?revision=$revision$$endif$">Raw page source</a></li>
+      <li><a href="$base$$pageUrl$?printable$if(revision)$&amp;revision=$revision$$endif$">Printable version</a></li>
+      <li><a href="$base$/_delete$pageUrl$">Delete this page</a></li>
+    </ul>
+    $exportbox$
+  </fieldset>
+</div>
diff --git a/data/templates/sitenav.st b/data/templates/sitenav.st
new file mode 100644
--- /dev/null
+++ b/data/templates/sitenav.st
@@ -0,0 +1,22 @@
+<div class="sitenav">
+  <fieldset>
+    <legend>Site</legend>
+    <ul>
+      <li><a href="$base$/">Front page</a></li>
+      <li><a href="$base$/_index">All pages</a></li>
+      <li><a href="$base$/_categories">Categories</a></li>
+      <li><a href="$base$/_random">Random page</a></li>
+      <li><a href="$base$/_activity">Recent activity</a></li>
+      <li><a href="$base$/_upload">Upload a file</a></li>
+      <li><a href="$base$/Help">Help</a></li>
+    </ul>
+    <form action="$base$/_search" method="post" id="searchform">
+     <input type="text" name="patterns" id="patterns"/>
+     <input type="submit" name="search" id="search" value="Search"/>
+    </form>
+    <form action="$base$/_go" method="post" id="goform">
+      <input type="text" name="gotopage" id="gotopage"/>
+      <input type="submit" name="go" id="go" value="Go"/>
+    </form>
+  </fieldset>
+</div>
diff --git a/data/templates/userbox.st b/data/templates/userbox.st
new file mode 100644
--- /dev/null
+++ b/data/templates/userbox.st
@@ -0,0 +1,9 @@
+<div id="userbox">
+  <noscript>
+    <a href="$base$/_login">Login</a>
+    <a href="$base$/_logout">Logout</a>
+  </noscript>
+  &nbsp;
+  <a id="loginlink" class="login" href="$base$/_login">Login / Get an account</a>
+  <a id="logoutlink" class="login" href="$base$/_logout">Logout <span id="username"></span></a>
+</div>
diff --git a/expireGititCache.hs b/expireGititCache.hs
new file mode 100644
--- /dev/null
+++ b/expireGititCache.hs
@@ -0,0 +1,66 @@
+{-
+expireGititCache - (C) 2009 John MacFarlane, licensed under the GPL
+
+This program is designed to be used in post-update hooks and other scripts.
+
+Usage:  expireGititCache base-url [file..]
+
+Example:
+
+    expireGititCache http://localhost:5001 page1.page foo/bar.hs "Front Page.page"
+
+will produce POST requests to http://localhost:5001/_expire/page1,
+http://localhost:5001/_expire/foo/bar.hs, and
+http://localhost:5001/_expire/Front Page.
+
+Return statuses:
+
+0   -> the cached page was successfully expired (or was not cached in the first place)
+1   -> fewer than two arguments were supplied
+3   -> did not receive a 200 OK response from the request
+5   -> could not parse the uri
+
+-}
+
+module Main
+where
+import Network.HTTP
+import System.Environment
+import Network.URI
+import System.FilePath
+import Control.Monad
+import System.IO
+import System.Exit
+
+main :: IO ()
+main = do
+  args <- getArgs
+  (uriString : files) <- if length args < 2
+                            then usageMessage >> return [""]
+                            else return args
+  uri <- case parseURI uriString of
+             Just u  -> return u
+             Nothing -> do
+               hPutStrLn stderr ("Could not parse URI " ++ uriString)
+               exitWith (ExitFailure 5)
+  forM_ files (expireFile uri)  
+
+usageMessage :: IO ()
+usageMessage = do
+  hPutStrLn stderr $ "Usage: expireGititCache base-url [file..]\n" ++
+    "Example: expireGititCache http://localhost:5001 page1.page foo/bar.hs"
+  exitWith (ExitFailure 1)
+
+expireFile :: URI -> FilePath -> IO ()
+expireFile uri file = do
+  let path' = if takeExtension file == ".page"
+                 then dropExtension file
+                 else file
+  let uri' = uri{uriPath = "/_expire/" ++ urlEncode path'}
+  resResp <- simpleHTTP Request{rqURI = uri', rqMethod = POST, rqHeaders = [], rqBody = ""}
+  case resResp of
+       Left connErr    -> error $ show connErr
+       Right (Response (2,0,0) _ _ _) -> return ()
+       _ -> do
+         hPutStrLn stderr ("Request for " ++ show uri' ++ " did not return success status")
+         exitWith (ExitFailure 3)
diff --git a/gitit.cabal b/gitit.cabal
--- a/gitit.cabal
+++ b/gitit.cabal
@@ -1,18 +1,38 @@
 name:                gitit
-version:             0.5.3
+version:             0.6.1
 Cabal-version:       >= 1.2
 build-type:          Simple
-synopsis:            Wiki using HAppS, git or darcs, and pandoc.
-description:         Gitit is a wiki program. Pages and uploaded files
-                     are stored in a git or darcs repository and may be modified
-                     either by using the VCS's command-line tools or through
-                     the wiki's web interface. Pandoc's extended version
-                     of markdown is used as a markup language. Pages
-                     can be exported in a number of different formats,
-                     including LaTeX, RTF, OpenOffice ODT, and MediaWiki
-                     markup. Gitit can be configured to display TeX math
-                     (using jsMath) and highlighted source code (using
-                     highlighting-kate).
+synopsis:            Wiki using happstack, git or darcs, and pandoc.
+description:         Gitit is a wiki backed by a git or darcs filestore.
+                     Pages and uploaded files can be modified either
+                     directly via the VCS's command-line tools or through
+                     the wiki's web interface. Pandoc is used for markup
+                     processing, so pages may be written in
+                     (extended) markdown, reStructuredText, LaTeX, HTML,
+                     or literate Haskell, and exported in ten different
+                     formats, including LaTeX, ConTeXt, DocBook, RTF,
+                     OpenOffice ODT, and MediaWiki markup.
+                     .
+                     Notable features include
+                     .
+                     * plugins: dynamically loaded page
+                       transformations written in Haskell (see
+                       "Network.Gitit.Interface")
+                     .
+                     * conversion of TeX math to MathML for display in
+                       web browsers
+                     .
+                     * syntax highlighting of source code
+                       files and code snippets
+                     .
+                     * Atom feeds (site-wide and per-page)
+                     .
+                     * a library, "Network.Gitit", that makes it simple
+                       to include a gitit wiki in any happstack application
+                     .
+                     You can see a running demo at <http://gitit.johnmacfarlane.net>.
+                     .
+                     For usage information:  @gitit --help@
 
 category:            Network
 license:             GPL
@@ -22,46 +42,88 @@
 bug-reports:         http://code.google.com/p/gitit/issues/list
 homepage:            http://github.com/jgm/gitit/tree/master
 stability:           experimental 
-data-files:          css/screen.css, css/print.css, css/ie.css, css/hk-pyg.css,
-                     img/gitit-dog.png,
-                     img/icons/folder.png, img/icons/page.png,
-                     img/icons/cross.png, img/icons/doc.png, img/icons/email.png,
-                     img/icons/external.png, img/icons/external.png, img/icons/feed.png,
-                     img/icons/folder.png, img/icons/im.png, img/icons/key.png,
-                     img/icons/page.png, img/icons/pdf.png, img/icons/tick.png,
-                     img/icons/xls.png,
-                     js/dragdiff.js, js/folding.js,
-                     js/jquery.min.js, js/uploadForm.js,
-                     js/jquery-ui.packed.js,
-                     js/preview.js, js/search.js,
+data-files:          data/static/css/screen.css, data/static/css/print.css,
+                     data/static/css/ie.css, data/static/css/hk-pyg.css,
+                     data/static/css/reset-fonts-grids.css, data/static/css/base-min.css,
+                     data/static/css/custom.css,
+                     data/static/img/gitit-dog.png,
+                     data/static/img/icons/folder.png, data/static/img/icons/page.png,
+                     data/static/js/dragdiff.js, data/static/js/jquery.min.js,
+                     data/static/js/uploadForm.js, data/static/js/jquery-ui.packed.js,
+                     data/static/js/jquery.hotkeys-0.7.9.min.js,
+                     data/static/js/preview.js, data/static/js/search.js,
+                     data/static/js/MathMLinHTML.js,
+                     data/static/robots.txt,
                      data/post-update, data/FrontPage.page, data/Help.page,
-                     data/template.html, data/SampleConfig.hs,
-                     CHANGES, README.markdown, BLUETRIP-LICENSE, TANGOICONS
+                     data/markup.Markdown, data/markup.RST,
+                     data/markup.HTML, data/markup.LaTeX,
+                     data/default.conf,
+                     data/templates/page.st, data/templates/content.st,
+                     data/templates/userbox.st, data/templates/footer.st,
+                     data/templates/logo.st, data/templates/markuphelp.st,
+                     data/templates/pagetools.st, data/templates/sitenav.st,
+                     data/templates/messages.st, data/templates/listitem.st,
+                     data/templates/expire.st, data/templates/getuser.st,
+                     data/markupHelp/Markdown, data/markupHelp/Markdown+LHS,
+                     data/markupHelp/RST, data/markupHelp/RST+LHS,
+                     data/markupHelp/LaTeX, data/markupHelp/LaTeX+LHS,
+                     data/markupHelp/HTML,
+                     plugins/CapitalizeEmphasis.hs,
+                     plugins/Dot.hs,
+                     plugins/ImgTex.hs,
+                     plugins/Interwiki.hs,
+                     plugins/Deprofanizer.hs,
+                     plugins/WebArchiver.hs,
+                     plugins/ShowUser.hs,
+                     plugins/Signature.hs,
+                     CHANGES, README.markdown, YUI-LICENSE, BLUETRIP-LICENSE, TANGOICONS
 
-Flag happstack
-  description:       Use Happstack instead of HAppS for the server.
-  default:           False
+Flag plugins
+  description:       Compile in support for plugins.  This will increase the size of
+                     the executable and the memory it uses, so those who will not need
+                     plugins should disable this flag.
+  default:           True
 
+Library
+  hs-source-dirs:    .
+  exposed-modules:   Network.Gitit, Network.Gitit.ContentTransformer,
+                     Network.Gitit.Types, Network.Gitit.Framework,
+                     Network.Gitit.Initialize, Network.Gitit.Config
+  other-modules:     Network.Gitit.Layout, Network.Gitit.Cache, Network.Gitit.State,
+                     Paths_gitit, Network.Gitit.Server, Network.Gitit.Export,
+                     Network.Gitit.Util, Network.Gitit.Handlers, Network.Gitit.Plugins,
+                     Network.Gitit.Authentication, Network.Gitit.Page, Network.Gitit.Feed
+  if flag(plugins)
+    exposed-modules: Network.Gitit.Interface
+  build-depends:     base >= 3, pandoc >= 1.1, filepath
+  ghc-options:       -Wall
+  ghc-prof-options:  -auto-all -caf-all
+
 Executable           gitit
   hs-source-dirs:    .
-  main-is:           Gitit.hs 
-  other-modules:     Gitit.State, Gitit.Server, Gitit.Util, Gitit.Export, Gitit.Layout,
-                     Gitit.Convert, Gitit.Initialize, Gitit.Config, Gitit.Framework,
-                     Paths_gitit
-  build-depends:     base >=3, parsec < 3, pretty, xhtml, containers, pandoc
-                     >= 1.1, process, filepath, directory, mtl, cgi,
+  main-is:           gitit.hs
+  build-depends:     base >=3 && < 5, parsec < 3, pretty, xhtml, containers,
+                     pandoc >= 1.2, process, filepath, directory, mtl, cgi,
                      network, old-time, highlighting-kate, bytestring,
                      utf8-string, SHA > 1, HTTP, HStringTemplate, random,
-                     network >= 2.1.0.0, recaptcha >= 0.1, filestore, datetime, zlib
+                     network >= 2.1.0.0, recaptcha >= 0.1, filestore >= 0.3.2,
+                     datetime, zlib, url, happstack-server >= 0.3.3 && < 0.4,
+                     happstack-util >= 0.3.2 && < 0.4, xml >= 1.3.4,
+                     hslogger >= 1 && < 1.1, ConfigFile >= 1, feed >= 0.3.6,
+                     cautious-file >= 0.1.5 && < 0.2, texmath
   if impl(ghc >= 6.10)
     build-depends:   base >= 4, syb
-  if flag(happstack)
-    build-depends:   happstack-server >= 0.1 && < 0.2
-    cpp-options:     -D_HAPPSTACK
-  else
-    build-depends:   HAppS-Server >= 0.9.3 && < 0.9.4
-    cpp-options:     -D_HAPPS
+  if flag(plugins)
+    build-depends:   ghc, ghc-paths
+    cpp-options:     -D_PLUGINS
   ghc-options:       -Wall -threaded
-  cpp-options:       -D_VERSION="0.5.3"
-  ghc-prof-options:  -auto-all
+  ghc-prof-options:  -auto-all -caf-all
+
+Executable           expireGititCache
+  hs-source-dirs:    .
+  main-is:           expireGititCache.hs
+  build-depends:     base >=3 && < 5, HTTP, url, filepath
+  if impl(ghc >= 6.10)
+    build-depends:   base >= 4, syb
+  ghc-options:       -Wall
 
diff --git a/gitit.hs b/gitit.hs
new file mode 100644
--- /dev/null
+++ b/gitit.hs
@@ -0,0 +1,71 @@
+{-
+Copyright (C) 2008 John MacFarlane <jgm@berkeley.edu>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+-}
+
+module Main where
+
+import Network.Gitit
+import Network.Gitit.Server
+import Network.Gitit.Initialize (createStaticIfMissing, createRepoIfMissing)
+import Prelude hiding (writeFile, readFile, catch)
+import System.Directory
+import Network.Gitit.Config (getConfigFromOpts)
+import Data.Maybe (isNothing)
+import Control.Monad.Reader
+import System.Log.Logger (Priority(..), setLevel, setHandlers,
+        getLogger, saveGlobalLogger)
+import System.Log.Handler.Simple (fileHandler)
+import Data.Char (toLower)
+
+main :: IO ()
+main = do
+
+  -- parse options to get config file
+  conf <- getConfigFromOpts
+
+  -- check for external programs that are needed
+  let repoProg = map toLower $ show $ repositoryType conf
+  let prereqs = ["grep", repoProg]
+  forM_ prereqs $ \prog ->
+    findExecutable prog >>= \mbFind ->
+    when (isNothing mbFind) $ error $
+      "Required program '" ++ prog ++ "' not found in system path."
+
+  -- set up logging
+  let level = if debugMode conf then DEBUG else logLevel conf
+  logFileHandler <- fileHandler (logFile conf) level
+  serverLogger <- getLogger "Happstack.Server.AccessLog.Combined"
+  gititLogger <- getLogger "gitit"
+  saveGlobalLogger $ setLevel level $ setHandlers [logFileHandler] serverLogger
+  saveGlobalLogger $ setLevel level $ setHandlers [logFileHandler] gititLogger
+
+  let conf' = conf{logLevel = level}
+
+  -- setup the page repository, template, and static files, if they don't exist
+  createRepoIfMissing conf'
+  createStaticIfMissing conf'
+  createTemplateIfMissing conf'
+
+  -- initialize state
+  initializeGititState conf'
+
+  let serverConf = Conf { validator = Nothing, port = portNumber conf' }
+  -- start the server
+  simpleHTTP serverConf $ msum [
+      wiki conf'
+    , dir "_reloadTemplates" reloadTemplates
+    ]
diff --git a/img/gitit-dog.png b/img/gitit-dog.png
deleted file mode 100644
Binary files a/img/gitit-dog.png and /dev/null differ
diff --git a/img/icons/cross.png b/img/icons/cross.png
deleted file mode 100644
Binary files a/img/icons/cross.png and /dev/null differ
diff --git a/img/icons/doc.png b/img/icons/doc.png
deleted file mode 100644
Binary files a/img/icons/doc.png and /dev/null differ
diff --git a/img/icons/email.png b/img/icons/email.png
deleted file mode 100644
Binary files a/img/icons/email.png and /dev/null differ
diff --git a/img/icons/external.png b/img/icons/external.png
deleted file mode 100644
Binary files a/img/icons/external.png and /dev/null differ
diff --git a/img/icons/feed.png b/img/icons/feed.png
deleted file mode 100644
Binary files a/img/icons/feed.png and /dev/null differ
diff --git a/img/icons/folder.png b/img/icons/folder.png
deleted file mode 100644
Binary files a/img/icons/folder.png and /dev/null differ
diff --git a/img/icons/im.png b/img/icons/im.png
deleted file mode 100644
Binary files a/img/icons/im.png and /dev/null differ
diff --git a/img/icons/key.png b/img/icons/key.png
deleted file mode 100644
Binary files a/img/icons/key.png and /dev/null differ
diff --git a/img/icons/page.png b/img/icons/page.png
deleted file mode 100644
Binary files a/img/icons/page.png and /dev/null differ
diff --git a/img/icons/pdf.png b/img/icons/pdf.png
deleted file mode 100644
Binary files a/img/icons/pdf.png and /dev/null differ
diff --git a/img/icons/tick.png b/img/icons/tick.png
deleted file mode 100644
Binary files a/img/icons/tick.png and /dev/null differ
diff --git a/img/icons/xls.png b/img/icons/xls.png
deleted file mode 100644
Binary files a/img/icons/xls.png and /dev/null differ
diff --git a/js/dragdiff.js b/js/dragdiff.js
deleted file mode 100644
--- a/js/dragdiff.js
+++ /dev/null
@@ -1,21 +0,0 @@
-$(document).ready(function(){
-    $("#content").prepend("<p>Drag one revision onto another to see differences.</p>");
-    $(".difflink").draggable({helper: "clone"}); 
-    $(".difflink").droppable({
-         accept: ".difflink",
-         drop: function(ev, ui) {
-            var targetOrder = parseInt($(this).attr("order"));
-            var sourceOrder = parseInt($(ui.draggable).attr("order"));
-            if (targetOrder < sourceOrder) {
-                var fromRev = $(this).attr("revision");
-                var toRev   = $(ui.draggable).attr("revision");
-            } else {
-                var toRev   = $(this).attr("revision");
-                var fromRev = $(ui.draggable).attr("revision");
-            };
-            location.href = location.protocol + '//' + location.host + location.pathname +
-                             '?diff&from=' + fromRev + '&to=' + toRev;
-            }
-        });
-});
-
diff --git a/js/folding.js b/js/folding.js
deleted file mode 100644
--- a/js/folding.js
+++ /dev/null
@@ -1,28 +0,0 @@
-// Execute this after the site is loaded.
-// from http://homework.nwsnet.de/news/view/31-turn-nested-lists-into-a-collapsible-tree-with-jquery
-$(function() {
-    // Find list items representing folders and
-    // style them accordingly.  Also, turn them
-    // into links that can expand/collapse the
-    // tree leaf.
-    $('ul.folding li > ul').each(function() {
-        // Find this list's parent list item.
-        var parent_li = $(this).parent('li');
-
-        // Style the list item as folder.
-        parent_li.addClass('folder');
-
-        // Temporarily remove the list from the
-        // parent list item, wrap the remaining
-        // text in an anchor, then reattach it.
-        var sub_ul = $(this).remove();
-        parent_li.wrapInner('<a/>').find('a').click(function() {
-            // Make the anchor toggle the leaf display.
-            sub_ul.toggle();
-        });
-        parent_li.append(sub_ul);
-    });
-
-    // Hide all lists except the outermost.
-    $('ul ul').hide();
-});
diff --git a/js/jquery-ui.packed.js b/js/jquery-ui.packed.js
deleted file mode 100644
--- a/js/jquery-ui.packed.js
+++ /dev/null
@@ -1,1 +0,0 @@
-eval(function(p,a,c,k,e,d){e=function(c){return(c<a?'':e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('(6(D){b A=D.4e.1B;D.4e.1B=6(){D("*",4).1j(4).21("1B");d A.2W(4,4g)};6 C(E){6 G(H){b I=H.24;d(I.2G!="3I"&&I.6g!="3F")}b F=G(E);(F&&D.1c(D.6m(E,"1X"),6(){d(F=G(4))}));d F}D.1x(D.6e[":"],{p:6(F,G,E){d D.p(F,E[3])},6v:6(F,G,E){b H=F.62.61();d(F.64>=0&&(("a"==H&&F.1q)||(/5A|2L|67|5B/.18(H)&&"3F"!=F.4R&&!F.v))&&C(F))}});D.6U={6w:8,6S:20,6X:6Z,6C:17,6D:46,6x:40,75:35,6Y:13,6V:27,76:36,6t:45,68:37,69:6c,65:6b,6s:6i,6r:6y,5Z:60,6o:6u,6n:34,6l:33,6q:6k,6f:39,6h:16,63:32,5Y:9,6d:38};6 B(I,E,J,H){6 G(L){b K=D[I][E][L]||[];d(2T K=="2S"?K.4f(/,?\\s+/):K)}b F=G("5m");5(H.X==1&&2T H[0]=="2S"){F=F.5l(G("5P"))}d(D.3q(J,F)!=-1)}D.2g=6(E,F){b G=E.4f(".")[0];E=E.4f(".")[1];D.4e[E]=6(K){b I=(2T K=="2S"),J=5f.2y.6a.12(4g,1);5(I&&K.6W(0,1)=="4Q"){d 4}5(I&&B(G,E,K,J)){b H=D.p(4[0],E);d(H?H[K].2W(H,J):1Q)}d 4.1c(6(){b L=D.p(4,E);(!L&&!I&&D.p(4,E,6T D[G][E](4,K)));(L&&I&&D.3s(L[K])&&L[K].2W(L,J))})};D[G][E]=6(I,J){b H=4;4.2v=E;4.4N=D[G][E].6Q||E;4.5J=G+"-"+E;4.7=D.1x({},D.2g.2B,D[G][E].2B,D.5T&&D.5T.4a(I)[E],J);4.k=D(I).1A("74."+E,6(M,K,L){d H.2j(K,L)}).1A("71."+E,6(L,K){d H.4k(K)}).1A("1B",6(){d H.2f()});4.3f()};D[G][E].2y=D.1x({},D.2g.2y,F);D[G][E].5P="5O"};D.2g.2y={3f:6(){},2f:6(){4.k.2E(4.2v)},5O:6(G,H){b F=G,E=4;5(2T G=="2S"){5(H===1Q){d 4.4k(G)}F={};F[G]=H}D.1c(F,6(I,J){E.2j(I,J)})},4k:6(E){d 4.7[E]},2j:6(E,F){4.7[E]=F;5(E=="v"){4.k[F?"Z":"1f"](4.5J+"-v")}},4j:6(){4.2j("v",o)},4m:6(){4.2j("v",Y)},2b:6(F,H,G){b E=(F==4.4N?F:4.4N+F);H=H||D.2h.6E({4R:E,30:4.k[0]});d 4.k.21(E,[H,G],4.7[F])}};D.2g.2B={v:o};D.c={1k:{1j:6(F,E,I){b H=D.c[F].2y;2A(b G 6z I){H.2Y[G]=H.2Y[G]||[];H.2Y[G].2U([E,I[G]])}},12:6(E,G,F){b I=E.2Y[G];5(!I){d}2A(b H=0;H<I.X;H++){5(E.7[I[H][0]]){I[H][1].2W(E.k,F)}}}},3e:{},n:6(E){5(D.c.3e[E]){d D.c.3e[E]}b F=D(\'<2V 56="c-6F">\').Z(E).n({w:"1p",j:"-5W",h:"-5W",2G:"44"}).1Y("1y");D.c.3e[E]=!!((!(/2i|3r/).18(F.n("2D"))||(/^[1-9]/).18(F.n("19"))||(/^[1-9]/).18(F.n("1d"))||!(/3I/).18(F.n("6K"))||!(/6H|6I\\(0, 0, 0, 0\\)/).18(F.n("6R"))));6J{D("1y").4a(0).52(F.4a(0))}6L(G){}d D.c.3e[E]},6N:6(E){d D(E).1J("3c","5k").n("5j","3I").1A("5h.c",6(){d o})},6M:6(E){d D(E).1J("3c","6G").n("5j","").1D("5h.c")},6A:6(H,E){5(D(H).n("1F")=="3F"){d o}b G=(E&&E=="h")?"1n":"1m",F=o;5(H[G]>0){d Y}H[G]=1;F=(H[G]>0);H[G]=0;d F}};D.c.4q={5r:6(){b E=4;4.k.1A("6B."+4.2v,6(F){d E.53(F)});5(D.1I.2k){4.4Z=4.k.1J("3c");4.k.1J("3c","5k")}4.6P=o},5y:6(){4.k.1D("."+4.2v);(D.1I.2k&&4.k.1J("3c",4.4Z))},53:6(G){(4.28&&4.3i(G));4.3J=G;b E=4,H=(G.70==1),F=(2T 4.7.3S=="2S"?D(G.30).3X().1j(G.30).1S(4.7.3S).X:o);5(!H||F||!4.3G(G)){d Y}4.3K=!4.7.3x;5(!4.3K){4.72=3U(6(){E.3K=Y},4.7.3x)}5(4.4B(G)&&4.4A(G)){4.28=(4.3j(G)!==o);5(!4.28){G.73();d Y}}4.4K=6(I){d E.55(I)};4.4I=6(I){d E.3i(I)};D(u).1A("5F."+4.2v,4.4K).1A("5w."+4.2v,4.4I);d o},55:6(E){5(D.1I.2k&&!E.5B){d 4.3i(E)}5(4.28){4.2X(E);d o}5(4.4B(E)&&4.4A(E)){4.28=(4.3j(4.3J,E)!==o);(4.28?4.2X(E):4.3i(E))}d!4.28},3i:6(E){D(u).1D("5F."+4.2v,4.4K).1D("5w."+4.2v,4.4I);5(4.28){4.28=o;4.3p(E)}d o},4B:6(E){d(1o.4y(1o.1R(4.3J.2u-E.2u),1o.1R(4.3J.2m-E.2m))>=4.7.4G)},4A:6(E){d 4.3K},3j:6(E){},2X:6(E){},3p:6(E){},3G:6(E){d Y}};D.c.4q.2B={3S:15,4G:1,3x:0}})(3b);(6(A){A.2g("c.z",A.1x({},A.c.4q,{5H:6(C){b B=!4.7.2H||!A(4.7.2H,4.k).X?Y:o;A(4.7.2H,4.k).2Z("*").5K().1c(6(){5(4==C.30){B=Y}});d B},5a:6(){b C=4.7;b B=A.3s(C.t)?A(C.t.2W(4.k[0],[e])):(C.t=="4l"?4.k.4l():4.k);5(!B.3X("1y").X){B.1Y((C.1Y=="V"?4.k[0].1X:C.1Y))}5(B[0]!=4.k[0]&&!(/(26|1p)/).18(B.n("w"))){B.n("w","1p")}d B},3f:6(){5(4.7.t=="4C"&&!(/^(?:r|a|f)/).18(4.k.n("w"))){4.k[0].24.w="11"}(4.7.2O&&4.k.Z(4.7.2O+"-z"));(4.7.v&&4.k.Z("c-z-v"));4.5r()},3G:6(B){b C=4.7;5(4.t||C.v||A(B.30).3W(".c-6j-2H")){d o}4.2H=4.5H(B);5(!4.2H){d o}d Y},3j:6(D){b E=4.7;4.t=4.5a();5(A.c.14){A.c.14.2C=4}4.1w={h:(1i(4.k.n("66"),10)||0),j:(1i(4.k.n("6O"),10)||0)};4.1C=4.t.n("w");4.m=4.k.m();4.m={j:4.m.j-4.1w.j,h:4.m.h-4.1w.h};4.m.1a={h:D.2u-4.m.h,j:D.2m-4.m.j};4.5e();4.1U=4.t.1U();b B=4.1U.m();5(4.1U[0]==u.1y&&A.1I.7q){B={j:0,h:0}}4.m.V={j:B.j+(1i(4.1U.n("4z"),10)||0),h:B.h+(1i(4.1U.n("4x"),10)||0)};5(4.1C=="11"){b C=4.k.w();4.m.11={j:C.j-(1i(4.t.n("j"),10)||0)+4.2p.1m(),h:C.h-(1i(4.t.n("h"),10)||0)+4.2n.1n()}}1e{4.m.11={j:0,h:0}}4.2w=4.4J(D);4.4w();5(E.5d){4.57(E.5d)}A.1x(4,{4v:(4.1C=="1p"&&(!4.2p[0].1V||(/(2o|1y)/i).18(4.2p[0].1V))),4r:(4.1C=="1p"&&(!4.2n[0].1V||(/(2o|1y)/i).18(4.2n[0].1V))),4u:4.2p[0]!=4.1U[0]&&!(4.2p[0]==u&&(/(1y|2o)/i).18(4.1U[0].1V)),4s:4.2n[0]!=4.1U[0]&&!(4.2n[0]==u&&(/(1y|2o)/i).18(4.1U[0].1V))});5(E.U){4.5s()}4.23("22",D);4.4w();5(A.c.14&&!E.5E){A.c.14.3P(4,D)}4.t.Z("c-z-3E");4.2X(D);d Y},5e:6(){4.2p=6(B){3A{5(/2i|2c/.18(B.n("1F"))||(/2i|2c/).18(B.n("1F-y"))){d B}B=B.V()}3w(B[0].1X);d A(u)}(4.t);4.2n=6(B){3A{5(/2i|2c/.18(B.n("1F"))||(/2i|2c/).18(B.n("1F-x"))){d B}B=B.V()}3w(B[0].1X);d A(u)}(4.t)},57:6(B){5(B.h!=1Q){4.m.1a.h=B.h+4.1w.h}5(B.5n!=1Q){4.m.1a.h=4.1h.1d-B.5n+4.1w.h}5(B.j!=1Q){4.m.1a.j=B.j+4.1w.j}5(B.5q!=1Q){4.m.1a.j=4.1h.19-B.5q+4.1w.j}},4w:6(){4.1h={1d:4.t.5b(),19:4.t.5c()}},5s:6(){b E=4.7;5(E.U=="V"){E.U=4.t[0].1X}5(E.U=="u"||E.U=="2x"){4.U=[0-4.m.11.h-4.m.V.h,0-4.m.11.j-4.m.V.j,A(E.U=="u"?u:2x).1d()-4.m.11.h-4.m.V.h-4.1h.1d-4.1w.h-(1i(4.k.n("5t"),10)||0),(A(E.U=="u"?u:2x).19()||u.1y.1X.5u)-4.m.11.j-4.m.V.j-4.1h.19-4.1w.j-(1i(4.k.n("5v"),10)||0)]}5(!(/^(u|2x|V)$/).18(E.U)){b C=A(E.U)[0];b D=A(E.U).m();b B=(A(C).n("1F")!="3F");4.U=[D.h+(1i(A(C).n("4x"),10)||0)-4.m.11.h-4.m.V.h,D.j+(1i(A(C).n("4z"),10)||0)-4.m.11.j-4.m.V.j,D.h+(B?1o.4y(C.82,C.2K):C.2K)-(1i(A(C).n("4x"),10)||0)-4.m.11.h-4.m.V.h-4.1h.1d-4.1w.h-(1i(4.k.n("5t"),10)||0),D.j+(B?1o.4y(C.5u,C.2N):C.2N)-(1i(A(C).n("4z"),10)||0)-4.m.11.j-4.m.V.j-4.1h.19-4.1w.j-(1i(4.k.n("5v"),10)||0)]}},1L:6(C,D){5(!D){D=4.w}b B=C=="1p"?1:-1;d{j:(D.j+4.m.11.j*B+4.m.V.j*B-(4.1C=="26"||4.4v||4.4u?0:4.2p.1m())*B+(4.1C=="26"?A(u).1m():0)*B+4.1w.j*B),h:(D.h+4.m.11.h*B+4.m.V.h*B-(4.1C=="26"||4.4r||4.4s?0:4.2n.1n())*B+(4.1C=="26"?A(u).1n():0)*B+4.1w.h*B)}},4J:6(E){b F=4.7;b B={j:(E.2m-4.m.1a.j-4.m.11.j-4.m.V.j+(4.1C=="26"||4.4v||4.4u?0:4.2p.1m())-(4.1C=="26"?A(u).1m():0)),h:(E.2u-4.m.1a.h-4.m.11.h-4.m.V.h+(4.1C=="26"||4.4r||4.4s?0:4.2n.1n())-(4.1C=="26"?A(u).1n():0))};5(!4.2w){d B}5(4.U){5(B.h<4.U[0]){B.h=4.U[0]}5(B.j<4.U[1]){B.j=4.U[1]}5(B.h>4.U[2]){B.h=4.U[2]}5(B.j>4.U[3]){B.j=4.U[3]}}5(F.1Z){b D=4.2w.j+1o.5o((B.j-4.2w.j)/F.1Z[1])*F.1Z[1];B.j=4.U?(!(D<4.U[1]||D>4.U[3])?D:(!(D<4.U[1])?D-F.1Z[1]:D+F.1Z[1])):D;b C=4.2w.h+1o.5o((B.h-4.2w.h)/F.1Z[0])*F.1Z[0];B.h=4.U?(!(C<4.U[0]||C>4.U[2])?C:(!(C<4.U[0])?C-F.1Z[0]:C+F.1Z[0])):C}d B},2X:6(B){4.w=4.4J(B);4.1s=4.1L("1p");4.w=4.23("2a",B)||4.w;5(!4.7.3g||4.7.3g!="y"){4.t[0].24.h=4.w.h+"3v"}5(!4.7.3g||4.7.3g!="x"){4.t[0].24.j=4.w.j+"3v"}5(A.c.14){A.c.14.2a(4,B)}d o},3p:6(C){b D=o;5(A.c.14&&!4.7.5E){b D=A.c.14.2F(4,C)}5((4.7.2q=="81"&&!D)||(4.7.2q=="80"&&D)||4.7.2q===Y||(A.3s(4.7.2q)&&4.7.2q.12(4.k,D))){b B=4;A(4.t).4U(4.2w,1i(4.7.77,10)||5z,6(){B.23("1W",C);B.4L()})}1e{4.23("1W",C);4.4L()}d o},4L:6(){4.t.1f("c-z-3E");5(4.7.t!="4C"&&!4.3m){4.t.1B()}4.t=15;4.3m=o},2Y:{},3k:6(B){d{t:4.t,w:4.w,3T:4.1s,7:4.7}},23:6(C,B){A.c.1k.12(4,C,[B,4.3k()]);5(C=="2a"){4.1s=4.1L("1p")}d 4.k.21(C=="2a"?C:"2a"+C,[B,4.3k()],4.7[C])},2f:6(){5(!4.k.p("z")){d}4.k.2E("z").1D(".z").1f("c-z c-z-3E c-z-v");4.5y()}}));A.1x(A.c.z,{2B:{1Y:"V",3g:o,3S:":5A",3x:0,4G:1,t:"4C",2d:"3r",2O:"c"}});A.c.1k.1j("z","2D",{22:6(D,C){b B=A("1y");5(B.n("2D")){C.7.4E=B.n("2D")}B.n("2D",C.7.2D)},1W:6(C,B){5(B.7.4E){A("1y").n("2D",B.7.4E)}}});A.c.1k.1j("z","1K",{22:6(D,C){b B=A(C.t);5(B.n("1K")){C.7.4F=B.n("1K")}B.n("1K",C.7.1K)},1W:6(C,B){5(B.7.4F){A(B.t).n("1K",B.7.4F)}}});A.c.1k.1j("z","1O",{22:6(D,C){b B=A(C.t);5(B.n("1O")){C.7.4M=B.n("1O")}B.n("1O",C.7.1O)},1W:6(C,B){5(B.7.4M){A(B.t).n("1O",B.7.4M)}}});A.c.1k.1j("z","3d",{22:6(C,B){A(B.7.3d===Y?"7V":B.7.3d).1c(6(){A(\'<2V 56="c-z-3d" 24="7U: #7T;"></2V>\').n({1d:4.2K+"3v",19:4.2N+"3v",w:"1p",1O:"0.7W",1K:7X}).n(A(4).m()).1Y("1y")})},1W:6(C,B){A("2V.c-z-3d").1c(6(){4.1X.52(4)})}});A.c.1k.1j("z","2c",{22:6(D,C){b E=C.7;b B=A(4).p("z");E.1N=E.1N||20;E.1P=E.1P||20;B.1H=6(F){3A{5(/2i|2c/.18(F.n("1F"))||(/2i|2c/).18(F.n("1F-y"))){d F}F=F.V()}3w(F[0].1X);d A(u)}(4);B.1M=6(F){3A{5(/2i|2c/.18(F.n("1F"))||(/2i|2c/).18(F.n("1F-x"))){d F}F=F.V()}3w(F[0].1X);d A(u)}(4);5(B.1H[0]!=u&&B.1H[0].1V!="3y"){B.4c=B.1H.m()}5(B.1M[0]!=u&&B.1M[0].1V!="3y"){B.4b=B.1M.m()}},2a:6(E,D){b F=D.7,C=o;b B=A(4).p("z");5(B.1H[0]!=u&&B.1H[0].1V!="3y"){5((B.4c.j+B.1H[0].2N)-E.2m<F.1N){B.1H[0].1m=C=B.1H[0].1m+F.1P}5(E.2m-B.4c.j<F.1N){B.1H[0].1m=C=B.1H[0].1m-F.1P}}1e{5(E.2m-A(u).1m()<F.1N){C=A(u).1m(A(u).1m()-F.1P)}5(A(2x).19()-(E.2m-A(u).1m())<F.1N){C=A(u).1m(A(u).1m()+F.1P)}}5(B.1M[0]!=u&&B.1M[0].1V!="3y"){5((B.4b.h+B.1M[0].2K)-E.2u<F.1N){B.1M[0].1n=C=B.1M[0].1n+F.1P}5(E.2u-B.4b.h<F.1N){B.1M[0].1n=C=B.1M[0].1n-F.1P}}1e{5(E.2u-A(u).1n()<F.1N){C=A(u).1n(A(u).1n()-F.1P)}5(A(2x).1d()-(E.2u-A(u).1n())<F.1N){C=A(u).1n(A(u).1n()+F.1P)}}5(C!==o){A.c.14.3P(B,E)}}});A.c.1k.1j("z","1T",{22:6(D,C){b B=A(4).p("z");B.1v=[];A(C.7.1T.5I!=8f?(C.7.1T.8a||":p(z)"):C.7.1T).1c(6(){b F=A(4);b E=F.m();5(4!=B.k[0]){B.1v.2U({3H:4,1d:F.5b(),19:F.5c(),j:E.j,h:E.h})}})},2a:6(P,K){b E=A(4).p("z");b Q=K.7.8b||20;b O=K.3T.h,N=O+E.1h.1d,D=K.3T.j,C=D+E.1h.19;2A(b M=E.1v.X-1;M>=0;M--){b L=E.1v[M].h,J=L+E.1v[M].1d,I=E.1v[M].j,R=I+E.1v[M].19;5(!((L-Q<O&&O<J+Q&&I-Q<D&&D<R+Q)||(L-Q<O&&O<J+Q&&I-Q<C&&C<R+Q)||(L-Q<N&&N<J+Q&&I-Q<D&&D<R+Q)||(L-Q<N&&N<J+Q&&I-Q<C&&C<R+Q))){5(E.1v[M].3M){(E.7.1T.5C&&E.7.1T.5C.12(E.k,15,A.1x(E.3k(),{5U:E.1v[M].3H})))}E.1v[M].3M=o;3O}5(K.7.5X!="8c"){b B=1o.1R(I-C)<=Q;b S=1o.1R(R-D)<=Q;b G=1o.1R(L-N)<=Q;b H=1o.1R(J-O)<=Q;5(B){K.w.j=E.1L("11",{j:I-E.1h.19,h:0}).j}5(S){K.w.j=E.1L("11",{j:R,h:0}).j}5(G){K.w.h=E.1L("11",{j:0,h:L-E.1h.1d}).h}5(H){K.w.h=E.1L("11",{j:0,h:J}).h}}b F=(B||S||G||H);5(K.7.5X!="8d"){b B=1o.1R(I-D)<=Q;b S=1o.1R(R-C)<=Q;b G=1o.1R(L-O)<=Q;b H=1o.1R(J-N)<=Q;5(B){K.w.j=E.1L("11",{j:I,h:0}).j}5(S){K.w.j=E.1L("11",{j:R-E.1h.19,h:0}).j}5(G){K.w.h=E.1L("11",{j:0,h:L}).h}5(H){K.w.h=E.1L("11",{j:0,h:J-E.1h.1d}).h}}5(!E.1v[M].3M&&(B||S||G||H||F)){(E.7.1T.1T&&E.7.1T.1T.12(E.k,15,A.1x(E.3k(),{5U:E.1v[M].3H})))}E.1v[M].3M=(B||S||G||H||F)}}});A.c.1k.1j("z","5M",{22:6(D,C){b B=A(4).p("z");B.3L=[];A(C.7.5M).1c(6(){5(A.p(4,"4d")){b E=A.p(4,"4d");B.3L.2U({q:E,5V:E.7.2q});E.7n();E.23("3z",D,B)}})},1W:6(D,C){b B=A(4).p("z");A.1c(B.3L,6(){5(4.q.2R){4.q.2R=0;B.3m=Y;4.q.3m=o;5(4.5V){4.q.7.2q=Y}4.q.3p(D);4.q.k.21("7m",[D,A.1x(4.q.c(),{7l:B.k})],4.q.7.7k);4.q.7.t=4.q.7.4i}1e{4.q.23("3D",D,B)}})},2a:6(F,E){b D=A(4).p("z"),B=4;b C=6(K){b H=K.h,J=H+K.1d,I=K.j,G=I+K.19;d(H<(4.1s.h+4.m.1a.h)&&(4.1s.h+4.m.1a.h)<J&&I<(4.1s.j+4.m.1a.j)&&(4.1s.j+4.m.1a.j)<G)};A.1c(D.3L,6(G){5(C.12(D,4.q.7o)){5(!4.q.2R){4.q.2R=1;4.q.1r=A(B).4l().1Y(4.q.k).p("4d-3H",Y);4.q.7.4i=4.q.7.t;4.q.7.t=6(){d E.t[0]};F.30=4.q.1r[0];4.q.3G(F,Y);4.q.3j(F,Y,Y);4.q.m.1a.j=D.m.1a.j;4.q.m.1a.h=D.m.1a.h;4.q.m.V.h-=D.m.V.h-4.q.m.V.h;4.q.m.V.j-=D.m.V.j-4.q.m.V.j;D.23("7t",F)}5(4.q.1r){4.q.2X(F)}}1e{5(4.q.2R){4.q.2R=0;4.q.3m=Y;4.q.7.2q=o;4.q.3p(F,Y);4.q.7.t=4.q.7.4i;4.q.1r.1B();5(4.q.5R){4.q.5R.1B()}D.23("7S",F)}}})}});A.c.1k.1j("z","2P",{22:6(D,B){b C=A.7i(A(B.7.2P.7b)).4n(6(F,E){d(1i(A(F).n("1K"),10)||B.7.2P.3a)-(1i(A(E).n("1K"),10)||B.7.2P.3a)});A(C).1c(6(E){4.24.1K=B.7.2P.3a+E});4[0].24.1K=B.7.2P.3a+C.X}})})(3b);(6(A){A.2g("c.1u",{2j:6(B,C){5(B=="1t"){4.7.1t=C&&A.3s(C)?C:6(D){d D.3W(1t)}}1e{A.2g.2y.2j.2W(4,4g)}},3f:6(){b C=4.7,B=C.1t;4.1E=0;4.2s=1;4.7.1t=4.7.1t&&A.3s(4.7.1t)?4.7.1t:6(D){d D.3W(B)};4.3l={1d:4.k[0].2K,19:4.k[0].2N};A.c.14.2t[4.7.2d]=A.c.14.2t[4.7.2d]||[];A.c.14.2t[4.7.2d].2U(4);(4.7.2O&&4.k.Z(4.7.2O+"-1u"))},2Y:{},c:6(B){d{z:(B.1r||B.k),t:B.t,w:B.w,3T:B.1s,7:4.7,k:4.k}},2f:6(){b B=A.c.14.2t[4.7.2d];2A(b C=0;C<B.X;C++){5(B[C]==4){B.54(C,1)}}4.k.1f("c-1u-v").2E("1u").1D(".1u")},4P:6(C){b B=A.c.14.2C;5(!B||(B.1r||B.k)[0]==4.k[0]){d}5(4.7.1t.12(4.k,(B.1r||B.k))){A.c.1k.12(4,"4V",[C,4.c(B)]);4.k.21("7c",[C,4.c(B)],4.7.4V)}},4O:6(C){b B=A.c.14.2C;5(!B||(B.1r||B.k)[0]==4.k[0]){d}5(4.7.1t.12(4.k,(B.1r||B.k))){A.c.1k.12(4,"4X",[C,4.c(B)]);4.k.21("7d",[C,4.c(B)],4.7.4X)}},4Y:6(E,B){b C=B||A.c.14.2C;5(!C||(C.1r||C.k)[0]==4.k[0]){d o}b D=o;4.k.2Z(":p(1u)").5S(".c-z-3E").1c(6(){b F=A.p(4,"1u");5(F.7.5x&&A.c.2I(C,A.1x(F,{m:F.k.m()}),F.7.3B)){D=Y;d o}});5(D){d o}5(4.7.1t.12(4.k,(C.1r||C.k))){A.c.1k.12(4,"2F",[E,4.c(C)]);4.k.21("2F",[E,4.c(C)],4.7.2F);d 4.k}d o},51:6(C){b B=A.c.14.2C;A.c.1k.12(4,"3z",[C,4.c(B)]);5(B){4.k.21("7g",[C,4.c(B)],4.7.3z)}},5g:6(C){b B=A.c.14.2C;A.c.1k.12(4,"3D",[C,4.c(B)]);5(B){4.k.21("7f",[C,4.c(B)],4.7.3D)}}});A.1x(A.c.1u,{2B:{v:o,3B:"2I",2d:"3r",2O:"c"}});A.c.2I=6(I,E,J){5(!E.m){d o}b D=(I.1s||I.w.1p).h,C=D+I.1h.1d,K=(I.1s||I.w.1p).j,H=K+I.1h.19;b F=E.m.h,B=F+E.3l.1d,L=E.m.j,G=L+E.3l.19;7u(J){3R"7v":d(F<D&&C<B&&L<K&&H<G);3n;3R"2I":d(F<D+(I.1h.1d/2)&&C-(I.1h.1d/2)<B&&L<K+(I.1h.19/2)&&H-(I.1h.19/2)<G);3n;3R"7K":d(F<((I.1s||I.w.1p).h+(I.3N||I.m.1a).h)&&((I.1s||I.w.1p).h+(I.3N||I.m.1a).h)<B&&L<((I.1s||I.w.1p).j+(I.3N||I.m.1a).j)&&((I.1s||I.w.1p).j+(I.3N||I.m.1a).j)<G);3n;3R"7Q":d((K>=L&&K<=G)||(H>=L&&H<=G)||(K<L&&H>G))&&((D>=F&&D<=B)||(C>=F&&C<=B)||(D<F&&C>B));3n;3r:d o;3n}};A.c.14={2C:15,2t:{"3r":[]},3P:6(E,H){b B=A.c.14.2t[E.7.2d];b F=H?H.4R:15;b G=(E.1r||E.k).2Z(":p(1u)").5K();5N:2A(b D=0;D<B.X;D++){5(B[D].7.v||(E&&!B[D].7.1t.12(B[D].k,(E.1r||E.k)))){3O}2A(b C=0;C<G.X;C++){5(G[C]==B[D].k[0]){B[D].3l.19=0;3O 5N}}B[D].2M=B[D].k.n("2G")!="3I";5(!B[D].2M){3O}B[D].m=B[D].k.m();B[D].3l={1d:B[D].k[0].2K,19:B[D].k[0].2N};5(F=="7H"||F=="7G"){B[D].51.12(B[D],H)}}},2F:6(B,C){b D=o;A.1c(A.c.14.2t[B.7.2d],6(){5(!4.7){d}5(!4.7.v&&4.2M&&A.c.2I(B,4,4.7.3B)){D=4.4Y.12(4,C)}5(!4.7.v&&4.2M&&4.7.1t.12(4.k,(B.1r||B.k))){4.2s=1;4.1E=0;4.5g.12(4,C)}});d D},2a:6(B,C){5(B.7.7y){A.c.14.3P(B,C)}A.1c(A.c.14.2t[B.7.2d],6(){5(4.7.v||4.5p||!4.2M){d}b D=A.c.2I(B,4,4.7.3B);b G=!D&&4.1E==1?"2s":(D&&4.1E==0?"1E":15);5(!G){d}b F;5(4.7.5x){b E=4.k.3X(":p(1u):1G(0)");5(E.X){F=A.p(E[0],"1u");F.5p=(G=="1E"?1:0)}}5(F&&G=="1E"){F.1E=0;F.2s=1;F.4O.12(F,C)}4[G]=1;4[G=="2s"?"1E":"2s"]=0;4[G=="1E"?"4P":"4O"].12(4,C);5(F&&G=="2s"){F.2s=0;F.1E=1;F.4P.12(F,C)}})}};A.c.1k.1j("1u","3C",{3z:6(C,B){A(4).Z(B.7.3C)},3D:6(C,B){A(4).1f(B.7.3C)},2F:6(C,B){A(4).1f(B.7.3C)}});A.c.1k.1j("1u","3Q",{4V:6(C,B){A(4).Z(B.7.3Q)},4X:6(C,B){A(4).1f(B.7.3Q)},2F:6(C,B){A(4).1f(B.7.3Q)}})})(3b);(6(A){A.2g("c.l",{3f:6(){4.7.2h+=".l";4.3o(Y)},2j:6(B,C){5((/^W/).18(B)){4.2L(C)}1e{4.7[B]=C;4.3o()}},X:6(){d 4.$l.X},4h:6(B){d B.5D&&B.5D.2J(/\\s/g,"4Q").2J(/[^A-7C-7D-9\\-4Q:\\.]/g,"")||4.7.59+A.p(B)},c:6(C,B){d{7:4.7,7E:C,5G:B,29:4.$l.29(C)}},3o:6(P){4.$1g=A("42:7F(a[1q])",4.k);4.$l=4.$1g.47(6(){d A("a",4)[0]});4.$1b=A([]);b O=4,E=4.7;4.$l.1c(6(R,Q){5(Q.25&&Q.25.2J("#","")){O.$1b=O.$1b.1j(Q.25)}1e{5(A(Q).1J("1q")!="#"){A.p(Q,"1q.l",Q.1q);A.p(Q,"1z.l",Q.1q);b T=O.4h(Q);Q.1q="#"+T;b S=A("#"+T);5(!S.X){S=A(E.4p).1J("3u",T).Z(E.3h).7B(O.$1b[R-1]||O.k);S.p("2f.l",Y)}O.$1b=O.$1b.1j(S)}1e{E.v.2U(R+1)}}});5(P){4.k.Z(E.4D);4.$1b.1c(6(){b Q=A(4);Q.Z(E.3h)});5(E.W===1Q){5(4W.25){4.$l.1c(6(T,Q){5(Q.25==4W.25){E.W=T;5(A.1I.2k||A.1I.7A){b S=A(4W.25),R=S.1J("3u");S.1J("3u","");3U(6(){S.1J("3u",R)},5z)}7w(0,0);d o}})}1e{5(E.2e){b I=1i(A.2e("c-l-"+A.p(O.k[0])),10);5(I&&O.$l[I]){E.W=I}}1e{5(O.$1g.1S("."+E.1l).X){E.W=O.$1g.29(O.$1g.1S("."+E.1l)[0])}}}}E.W=E.W===15||E.W!==1Q?E.W:0;E.v=A.7x(E.v.5l(A.47(4.$1g.1S("."+E.2z),6(R,Q){d O.$1g.29(R)}))).4n();5(A.3q(E.W,E.v)!=-1){E.v.54(A.3q(E.W,E.v),1)}4.$1b.Z(E.2r);4.$1g.1f(E.1l);5(E.W!==15){4.$1b.1G(E.W).4S().1f(E.2r);4.$1g.1G(E.W).Z(E.1l);b B=6(){O.2b("4S",15,O.c(O.$l[E.W],O.$1b[E.W]))};5(A.p(4.$l[E.W],"1z.l")){4.1z(E.W,B)}1e{B()}}A(2x).1A("7z",6(){O.$l.1D(".l");O.$1g=O.$l=O.$1b=15})}1e{E.W=4.$1g.29(4.$1g.1S("."+E.1l)[0])}5(E.2e){A.2e("c-l-"+A.p(O.k[0]),E.W,E.2e)}2A(b H=0,N;N=4.$1g[H];H++){A(N)[A.3q(H,E.v)!=-1&&!A(N).31(E.1l)?"Z":"1f"](E.2z)}5(E.2l===o){4.$l.2E("2l.l")}b J,D,K={"3a-1d":0,4T:1},F="7O";5(E.2Q&&E.2Q.5I==5f){J=E.2Q[0]||K,D=E.2Q[1]||K}1e{J=D=E.2Q||K}b C={2G:"",1F:"",19:""};5(!A.1I.2k){C.1O=""}6 M(R,Q,S){Q.4U(J,J.4T||F,6(){Q.Z(E.2r).n(C);5(A.1I.2k&&J.1O){Q[0].24.1S=""}5(S){L(R,S,Q)}})}6 L(R,S,Q){5(D===K){S.n("2G","44")}S.4U(D,D.4T||F,6(){S.1f(E.2r).n(C);5(A.1I.2k&&D.1O){S[0].24.1S=""}O.2b("4S",15,O.c(R,S[0]))})}6 G(R,T,Q,S){T.Z(E.1l).7P().1f(E.1l);M(R,Q,S)}4.$l.1D(".l").1A(E.2h,6(){b T=A(4).3X("42:1G(0)"),Q=O.$1b.1S(":2M"),S=A(4.25);5((T.31(E.1l)&&!E.3Z)||T.31(E.2z)||A(4).31(E.3t)||O.2b("2L",15,O.c(4,S[0]))===o){4.48();d o}O.7.W=O.$l.29(4);5(E.3Z){5(T.31(E.1l)){O.7.W=15;T.1f(E.1l);O.$1b.1W();M(4,Q);4.48();d o}1e{5(!Q.X){O.$1b.1W();b R=4;O.1z(O.$l.29(4),6(){T.Z(E.1l).Z(E.4H);L(R,S)});4.48();d o}}}5(E.2e){A.2e("c-l-"+A.p(O.k[0]),O.7.W,E.2e)}O.$1b.1W();5(S.X){b R=4;O.1z(O.$l.29(4),Q.X?6(){G(R,T,Q,S)}:6(){T.Z(E.1l);L(R,S)})}1e{7N"3b 7M 7I: 7J 7L 7e."}5(A.1I.2k){4.48()}d o});5(!(/^1a/).18(E.2h)){4.$l.1A("1a.l",6(){d o})}},1j:6(E,D,C){5(C==1Q){C=4.$l.X}b G=4.7;b I=A(G.58.2J(/#\\{1q\\}/g,E).2J(/#\\{43\\}/g,D));I.p("2f.l",Y);b H=E.7h("#")==0?E.2J("#",""):4.4h(A("a:78-79",I)[0]);b F=A("#"+H);5(!F.X){F=A(G.4p).1J("3u",H).Z(G.2r).p("2f.l",Y)}F.Z(G.3h);5(C>=4.$1g.X){I.1Y(4.k);F.1Y(4.k[0].1X)}1e{I.5L(4.$1g[C]);F.5L(4.$1b[C])}G.v=A.47(G.v,6(K,J){d K>=C?++K:K});4.3o();5(4.$l.X==1){I.Z(G.1l);F.1f(G.2r);b B=A.p(4.$l[0],"1z.l");5(B){4.1z(C,B)}}4.2b("1j",15,4.c(4.$l[C],4.$1b[C]))},1B:6(B){b D=4.7,E=4.$1g.1G(B).1B(),C=4.$1b.1G(B).1B();5(E.31(D.1l)&&4.$l.X>1){4.2L(B+(B+1<4.$l.X?1:-1))}D.v=A.47(A.5Q(D.v,6(G,F){d G!=B}),6(G,F){d G>=B?--G:G});4.3o();4.2b("1B",15,4.c(E.2Z("a")[0],C[0]))},4j:6(B){b C=4.7;5(A.3q(B,C.v)==-1){d}b D=4.$1g.1G(B).1f(C.2z);5(A.1I.7a){D.n("2G","7j-44");3U(6(){D.n("2G","44")},0)}C.v=A.5Q(C.v,6(F,E){d F!=B});4.2b("4j",15,4.c(4.$l[B],4.$1b[B]))},4m:6(C){b B=4,D=4.7;5(C!=D.W){4.$1g.1G(C).Z(D.2z);D.v.2U(C);D.v.4n();4.2b("4m",15,4.c(4.$l[C],4.$1b[C]))}},2L:6(B){5(2T B=="2S"){B=4.$l.29(4.$l.1S("[1q$="+B+"]")[0])}4.$l.1G(B).7r(4.7.2h)},1z:6(G,K){b L=4,D=4.7,E=4.$l.1G(G),J=E[0],H=K==1Q||K===o,B=E.p("1z.l");K=K||6(){};5(!B||!H&&A.p(J,"2l.l")){K();d}b M=6(N){b O=A(N),P=O.2Z("*:7s");d P.X&&P.3W(":5S(7p)")&&P||O};b C=6(){L.$l.1S("."+D.3t).1f(D.3t).1c(6(){5(D.41){M(4).V().2o(M(4).p("43.l"))}});L.3V=15};5(D.41){b I=M(J).2o();M(J).7R("<4o></4o>").2Z("4o").p("43.l",I).2o(D.41)}b F=A.1x({},D.3Y,{5i:B,49:6(O,N){A(J.25).2o(O);C();5(D.2l){A.p(J,"2l.l",Y)}L.2b("1z",15,L.c(L.$l[G],L.$1b[G]));D.3Y.49&&D.3Y.49(O,N);K()}});5(4.3V){4.3V.8e();C()}E.Z(D.3t);3U(6(){L.3V=A.8g(F)},0)},5i:6(C,B){4.$l.1G(C).2E("2l.l").p("1z.l",B)},2f:6(){b B=4.7;4.k.1D(".l").1f(B.4D).2E("l");4.$l.1c(6(){b C=A.p(4,"1q.l");5(C){4.1q=C}b D=A(4).1D(".l");A.1c(["1q","1z","2l"],6(F,E){D.2E(E+".l")})});4.$1g.1j(4.$1b).1c(6(){5(A.p(4,"2f.l")){A(4).1B()}1e{A(4).1f([B.1l,B.4H,B.2z,B.3h,B.2r].88(" "))}})}});A.c.l.2B={3Z:o,2h:"1a",v:[],2e:15,41:"7Y&#7Z;",2l:o,59:"c-l-",3Y:{},2Q:15,58:\'<42><a 1q="#{1q}"><50>#{43}</50></a></42>\',4p:"<2V></2V>",4D:"c-l-89",1l:"c-l-W",4H:"c-l-3Z",2z:"c-l-v",3h:"c-l-5G",2r:"c-l-86",3t:"c-l-87"};A.c.l.5m="X";A.1x(A.c.l.2y,{4t:15,85:6(C,F){F=F||o;b B=4,E=4.7.W;6 G(){B.4t=84(6(){E=++E<B.$l.X?E:0;B.2L(E)},C)}6 D(H){5(!H||H.83){6p(B.4t)}}5(C){G();5(!F){4.$l.1A(4.7.2h,D)}1e{4.$l.1A(4.7.2h,6(){D();E=B.7.W;G()})}}1e{D();4.$l.1D(4.7.2h,D)}}})})(3b);',62,513,'||||this|if|function|options||||var|ui|return||||left||top|element|tabs|offset|css|false|data|instance|||helper|document|disabled|position|||draggable|||||||||||||||||||||containment|parent|selected|length|true|addClass||relative|call||ddmanager|null|||test|height|click|panels|each|width|else|removeClass|lis|helperProportions|parseInt|add|plugin|selectedClass|scrollTop|scrollLeft|Math|absolute|href|currentItem|positionAbs|accept|droppable|snapElements|margins|extend|body|load|bind|remove|cssPosition|unbind|isover|overflow|eq|overflowY|browser|attr|zIndex|_convertPositionTo|overflowX|scrollSensitivity|opacity|scrollSpeed|undefined|abs|filter|snap|offsetParent|tagName|stop|parentNode|appendTo|grid||triggerHandler|start|_propagate|style|hash|fixed||_mouseStarted|index|drag|_trigger|scroll|scope|cookie|destroy|widget|event|auto|_setData|msie|cache|pageY|scrollLeftParent|html|scrollTopParent|revert|hideClass|isout|droppables|pageX|widgetName|originalPosition|window|prototype|disabledClass|for|defaults|current|cursor|removeData|drop|display|handle|intersect|replace|offsetWidth|select|visible|offsetHeight|cssNamespace|stack|fx|isOver|string|typeof|push|div|apply|_mouseDrag|plugins|find|target|hasClass|||||||||min|jQuery|unselectable|iframeFix|cssCache|_init|axis|panelClass|_mouseUp|_mouseStart|uiHash|proportions|cancelHelperRemoval|break|_tabify|_mouseStop|inArray|default|isFunction|loadingClass|id|px|while|delay|HTML|activate|do|tolerance|activeClass|deactivate|dragging|hidden|_mouseCapture|item|none|_mouseDownEvent|mouseDelayMet|sortables|snapping|clickOffset|continue|prepareOffsets|hoverClass|case|cancel|absolutePosition|setTimeout|xhr|is|parents|ajaxOptions|unselect||spinner|li|label|block|||map|blur|success|get|overflowXOffset|overflowYOffset|sortable|fn|split|arguments|_tabId|_helper|enable|_getData|clone|disable|sort|em|panelTemplate|mouse|PAGEX_INCLUDES_SCROLL|OFFSET_PARENT_NOT_SCROLL_PARENT_X|rotation|OFFSET_PARENT_NOT_SCROLL_PARENT_Y|PAGEY_INCLUDES_SCROLL|cacheHelperProportions|borderLeftWidth|max|borderTopWidth|_mouseDelayMet|_mouseDistanceMet|original|navClass|_cursor|_zIndex|distance|unselectClass|_mouseUpDelegate|_generatePosition|_mouseMoveDelegate|_clear|_opacity|widgetEventPrefix|_out|_over|_|type|show|duration|animate|over|location|out|_drop|_mouseUnselectable|span|_activate|removeChild|_mouseDown|splice|_mouseMove|class|adjustOffsetFromHelper|tabTemplate|idPrefix|createHelper|outerWidth|outerHeight|cursorAt|cacheScrollParents|Array|_deactivate|selectstart|url|MozUserSelect|on|concat|getter|right|round|greedyChild|bottom|_mouseInit|setContainment|marginRight|scrollHeight|marginBottom|mouseup|greedy|_mouseDestroy|500|input|button|release|title|dropBehaviour|mousemove|panel|getHandle|constructor|widgetBaseClass|andSelf|insertBefore|connectToSortable|droppablesLoop|option|getterSetter|grep|placeholder|not|metadata|snapItem|shouldRevert|5000px|snapMode|TAB|NUMPAD_MULTIPLY|106|toLowerCase|nodeName|SPACE|tabIndex|NUMPAD_DECIMAL|marginLeft|textarea|LEFT|NUMPAD_ADD|slice|110|107|UP|expr|RIGHT|visibility|SHIFT|111|resizable|190|PAGE_UP|dir|PAGE_DOWN|NUMPAD_SUBTRACT|clearInterval|PERIOD|NUMPAD_ENTER|NUMPAD_DIVIDE|INSERT|109|tabbable|BACKSPACE|DOWN|108|in|hasScroll|mousedown|CONTROL|DELETE|fix|gen|off|transparent|rgba|try|backgroundImage|catch|enableSelection|disableSelection|marginTop|started|eventPrefix|backgroundColor|CAPS_LOCK|new|keyCode|ESCAPE|substring|COMMA|ENTER|188|which|getData|_mouseDelayTimer|preventDefault|setData|END|HOME|revertDuration|first|child|safari|group|dropover|dropout|identifier|dropdeactivate|dropactivate|indexOf|makeArray|inline|receive|sender|sortreceive|_refreshItems|containerCache|img|mozilla|trigger|last|toSortable|switch|fit|scrollTo|unique|refreshPositions|unload|opera|insertAfter|Za|z0|tab|has|sortactivate|dragstart|Tabs|Mismatching|pointer|fragment|UI|throw|normal|siblings|touch|wrapInner|fromSortable|fff|background|iframe|001|1000|Loading|8230|valid|invalid|scrollWidth|clientX|setInterval|rotate|hide|loading|join|nav|items|snapTolerance|inner|outer|abort|String|ajax'.split('|'),0,{}))
diff --git a/js/jquery.min.js b/js/jquery.min.js
deleted file mode 100644
--- a/js/jquery.min.js
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * jQuery 1.2.6 - New Wave Javascript
- *
- * Copyright (c) 2008 John Resig (jquery.com)
- * Dual licensed under the MIT (MIT-LICENSE.txt)
- * and GPL (GPL-LICENSE.txt) licenses.
- *
- * $Date: 2008-05-24 14:22:17 -0400 (Sat, 24 May 2008) $
- * $Rev: 5685 $
- */
-(function(){var _jQuery=window.jQuery,_$=window.$;var jQuery=window.jQuery=window.$=function(selector,context){return new jQuery.fn.init(selector,context);};var quickExpr=/^[^<]*(<(.|\s)+>)[^>]*$|^#(\w+)$/,isSimple=/^.[^:#\[\.]*$/,undefined;jQuery.fn=jQuery.prototype={init:function(selector,context){selector=selector||document;if(selector.nodeType){this[0]=selector;this.length=1;return this;}if(typeof selector=="string"){var match=quickExpr.exec(selector);if(match&&(match[1]||!context)){if(match[1])selector=jQuery.clean([match[1]],context);else{var elem=document.getElementById(match[3]);if(elem){if(elem.id!=match[3])return jQuery().find(selector);return jQuery(elem);}selector=[];}}else
-return jQuery(context).find(selector);}else if(jQuery.isFunction(selector))return jQuery(document)[jQuery.fn.ready?"ready":"load"](selector);return this.setArray(jQuery.makeArray(selector));},jquery:"1.2.6",size:function(){return this.length;},length:0,get:function(num){return num==undefined?jQuery.makeArray(this):this[num];},pushStack:function(elems){var ret=jQuery(elems);ret.prevObject=this;return ret;},setArray:function(elems){this.length=0;Array.prototype.push.apply(this,elems);return this;},each:function(callback,args){return jQuery.each(this,callback,args);},index:function(elem){var ret=-1;return jQuery.inArray(elem&&elem.jquery?elem[0]:elem,this);},attr:function(name,value,type){var options=name;if(name.constructor==String)if(value===undefined)return this[0]&&jQuery[type||"attr"](this[0],name);else{options={};options[name]=value;}return this.each(function(i){for(name in options)jQuery.attr(type?this.style:this,name,jQuery.prop(this,options[name],type,i,name));});},css:function(key,value){if((key=='width'||key=='height')&&parseFloat(value)<0)value=undefined;return this.attr(key,value,"curCSS");},text:function(text){if(typeof text!="object"&&text!=null)return this.empty().append((this[0]&&this[0].ownerDocument||document).createTextNode(text));var ret="";jQuery.each(text||this,function(){jQuery.each(this.childNodes,function(){if(this.nodeType!=8)ret+=this.nodeType!=1?this.nodeValue:jQuery.fn.text([this]);});});return ret;},wrapAll:function(html){if(this[0])jQuery(html,this[0].ownerDocument).clone().insertBefore(this[0]).map(function(){var elem=this;while(elem.firstChild)elem=elem.firstChild;return elem;}).append(this);return this;},wrapInner:function(html){return this.each(function(){jQuery(this).contents().wrapAll(html);});},wrap:function(html){return this.each(function(){jQuery(this).wrapAll(html);});},append:function(){return this.domManip(arguments,true,false,function(elem){if(this.nodeType==1)this.appendChild(elem);});},prepend:function(){return this.domManip(arguments,true,true,function(elem){if(this.nodeType==1)this.insertBefore(elem,this.firstChild);});},before:function(){return this.domManip(arguments,false,false,function(elem){this.parentNode.insertBefore(elem,this);});},after:function(){return this.domManip(arguments,false,true,function(elem){this.parentNode.insertBefore(elem,this.nextSibling);});},end:function(){return this.prevObject||jQuery([]);},find:function(selector){var elems=jQuery.map(this,function(elem){return jQuery.find(selector,elem);});return this.pushStack(/[^+>] [^+>]/.test(selector)||selector.indexOf("..")>-1?jQuery.unique(elems):elems);},clone:function(events){var ret=this.map(function(){if(jQuery.browser.msie&&!jQuery.isXMLDoc(this)){var clone=this.cloneNode(true),container=document.createElement("div");container.appendChild(clone);return jQuery.clean([container.innerHTML])[0];}else
-return this.cloneNode(true);});var clone=ret.find("*").andSelf().each(function(){if(this[expando]!=undefined)this[expando]=null;});if(events===true)this.find("*").andSelf().each(function(i){if(this.nodeType==3)return;var events=jQuery.data(this,"events");for(var type in events)for(var handler in events[type])jQuery.event.add(clone[i],type,events[type][handler],events[type][handler].data);});return ret;},filter:function(selector){return this.pushStack(jQuery.isFunction(selector)&&jQuery.grep(this,function(elem,i){return selector.call(elem,i);})||jQuery.multiFilter(selector,this));},not:function(selector){if(selector.constructor==String)if(isSimple.test(selector))return this.pushStack(jQuery.multiFilter(selector,this,true));else
-selector=jQuery.multiFilter(selector,this);var isArrayLike=selector.length&&selector[selector.length-1]!==undefined&&!selector.nodeType;return this.filter(function(){return isArrayLike?jQuery.inArray(this,selector)<0:this!=selector;});},add:function(selector){return this.pushStack(jQuery.unique(jQuery.merge(this.get(),typeof selector=='string'?jQuery(selector):jQuery.makeArray(selector))));},is:function(selector){return!!selector&&jQuery.multiFilter(selector,this).length>0;},hasClass:function(selector){return this.is("."+selector);},val:function(value){if(value==undefined){if(this.length){var elem=this[0];if(jQuery.nodeName(elem,"select")){var index=elem.selectedIndex,values=[],options=elem.options,one=elem.type=="select-one";if(index<0)return null;for(var i=one?index:0,max=one?index+1:options.length;i<max;i++){var option=options[i];if(option.selected){value=jQuery.browser.msie&&!option.attributes.value.specified?option.text:option.value;if(one)return value;values.push(value);}}return values;}else
-return(this[0].value||"").replace(/\r/g,"");}return undefined;}if(value.constructor==Number)value+='';return this.each(function(){if(this.nodeType!=1)return;if(value.constructor==Array&&/radio|checkbox/.test(this.type))this.checked=(jQuery.inArray(this.value,value)>=0||jQuery.inArray(this.name,value)>=0);else if(jQuery.nodeName(this,"select")){var values=jQuery.makeArray(value);jQuery("option",this).each(function(){this.selected=(jQuery.inArray(this.value,values)>=0||jQuery.inArray(this.text,values)>=0);});if(!values.length)this.selectedIndex=-1;}else
-this.value=value;});},html:function(value){return value==undefined?(this[0]?this[0].innerHTML:null):this.empty().append(value);},replaceWith:function(value){return this.after(value).remove();},eq:function(i){return this.slice(i,i+1);},slice:function(){return this.pushStack(Array.prototype.slice.apply(this,arguments));},map:function(callback){return this.pushStack(jQuery.map(this,function(elem,i){return callback.call(elem,i,elem);}));},andSelf:function(){return this.add(this.prevObject);},data:function(key,value){var parts=key.split(".");parts[1]=parts[1]?"."+parts[1]:"";if(value===undefined){var data=this.triggerHandler("getData"+parts[1]+"!",[parts[0]]);if(data===undefined&&this.length)data=jQuery.data(this[0],key);return data===undefined&&parts[1]?this.data(parts[0]):data;}else
-return this.trigger("setData"+parts[1]+"!",[parts[0],value]).each(function(){jQuery.data(this,key,value);});},removeData:function(key){return this.each(function(){jQuery.removeData(this,key);});},domManip:function(args,table,reverse,callback){var clone=this.length>1,elems;return this.each(function(){if(!elems){elems=jQuery.clean(args,this.ownerDocument);if(reverse)elems.reverse();}var obj=this;if(table&&jQuery.nodeName(this,"table")&&jQuery.nodeName(elems[0],"tr"))obj=this.getElementsByTagName("tbody")[0]||this.appendChild(this.ownerDocument.createElement("tbody"));var scripts=jQuery([]);jQuery.each(elems,function(){var elem=clone?jQuery(this).clone(true)[0]:this;if(jQuery.nodeName(elem,"script"))scripts=scripts.add(elem);else{if(elem.nodeType==1)scripts=scripts.add(jQuery("script",elem).remove());callback.call(obj,elem);}});scripts.each(evalScript);});}};jQuery.fn.init.prototype=jQuery.fn;function evalScript(i,elem){if(elem.src)jQuery.ajax({url:elem.src,async:false,dataType:"script"});else
-jQuery.globalEval(elem.text||elem.textContent||elem.innerHTML||"");if(elem.parentNode)elem.parentNode.removeChild(elem);}function now(){return+new Date;}jQuery.extend=jQuery.fn.extend=function(){var target=arguments[0]||{},i=1,length=arguments.length,deep=false,options;if(target.constructor==Boolean){deep=target;target=arguments[1]||{};i=2;}if(typeof target!="object"&&typeof target!="function")target={};if(length==i){target=this;--i;}for(;i<length;i++)if((options=arguments[i])!=null)for(var name in options){var src=target[name],copy=options[name];if(target===copy)continue;if(deep&&copy&&typeof copy=="object"&&!copy.nodeType)target[name]=jQuery.extend(deep,src||(copy.length!=null?[]:{}),copy);else if(copy!==undefined)target[name]=copy;}return target;};var expando="jQuery"+now(),uuid=0,windowData={},exclude=/z-?index|font-?weight|opacity|zoom|line-?height/i,defaultView=document.defaultView||{};jQuery.extend({noConflict:function(deep){window.$=_$;if(deep)window.jQuery=_jQuery;return jQuery;},isFunction:function(fn){return!!fn&&typeof fn!="string"&&!fn.nodeName&&fn.constructor!=Array&&/^[\s[]?function/.test(fn+"");},isXMLDoc:function(elem){return elem.documentElement&&!elem.body||elem.tagName&&elem.ownerDocument&&!elem.ownerDocument.body;},globalEval:function(data){data=jQuery.trim(data);if(data){var head=document.getElementsByTagName("head")[0]||document.documentElement,script=document.createElement("script");script.type="text/javascript";if(jQuery.browser.msie)script.text=data;else
-script.appendChild(document.createTextNode(data));head.insertBefore(script,head.firstChild);head.removeChild(script);}},nodeName:function(elem,name){return elem.nodeName&&elem.nodeName.toUpperCase()==name.toUpperCase();},cache:{},data:function(elem,name,data){elem=elem==window?windowData:elem;var id=elem[expando];if(!id)id=elem[expando]=++uuid;if(name&&!jQuery.cache[id])jQuery.cache[id]={};if(data!==undefined)jQuery.cache[id][name]=data;return name?jQuery.cache[id][name]:id;},removeData:function(elem,name){elem=elem==window?windowData:elem;var id=elem[expando];if(name){if(jQuery.cache[id]){delete jQuery.cache[id][name];name="";for(name in jQuery.cache[id])break;if(!name)jQuery.removeData(elem);}}else{try{delete elem[expando];}catch(e){if(elem.removeAttribute)elem.removeAttribute(expando);}delete jQuery.cache[id];}},each:function(object,callback,args){var name,i=0,length=object.length;if(args){if(length==undefined){for(name in object)if(callback.apply(object[name],args)===false)break;}else
-for(;i<length;)if(callback.apply(object[i++],args)===false)break;}else{if(length==undefined){for(name in object)if(callback.call(object[name],name,object[name])===false)break;}else
-for(var value=object[0];i<length&&callback.call(value,i,value)!==false;value=object[++i]){}}return object;},prop:function(elem,value,type,i,name){if(jQuery.isFunction(value))value=value.call(elem,i);return value&&value.constructor==Number&&type=="curCSS"&&!exclude.test(name)?value+"px":value;},className:{add:function(elem,classNames){jQuery.each((classNames||"").split(/\s+/),function(i,className){if(elem.nodeType==1&&!jQuery.className.has(elem.className,className))elem.className+=(elem.className?" ":"")+className;});},remove:function(elem,classNames){if(elem.nodeType==1)elem.className=classNames!=undefined?jQuery.grep(elem.className.split(/\s+/),function(className){return!jQuery.className.has(classNames,className);}).join(" "):"";},has:function(elem,className){return jQuery.inArray(className,(elem.className||elem).toString().split(/\s+/))>-1;}},swap:function(elem,options,callback){var old={};for(var name in options){old[name]=elem.style[name];elem.style[name]=options[name];}callback.call(elem);for(var name in options)elem.style[name]=old[name];},css:function(elem,name,force){if(name=="width"||name=="height"){var val,props={position:"absolute",visibility:"hidden",display:"block"},which=name=="width"?["Left","Right"]:["Top","Bottom"];function getWH(){val=name=="width"?elem.offsetWidth:elem.offsetHeight;var padding=0,border=0;jQuery.each(which,function(){padding+=parseFloat(jQuery.curCSS(elem,"padding"+this,true))||0;border+=parseFloat(jQuery.curCSS(elem,"border"+this+"Width",true))||0;});val-=Math.round(padding+border);}if(jQuery(elem).is(":visible"))getWH();else
-jQuery.swap(elem,props,getWH);return Math.max(0,val);}return jQuery.curCSS(elem,name,force);},curCSS:function(elem,name,force){var ret,style=elem.style;function color(elem){if(!jQuery.browser.safari)return false;var ret=defaultView.getComputedStyle(elem,null);return!ret||ret.getPropertyValue("color")=="";}if(name=="opacity"&&jQuery.browser.msie){ret=jQuery.attr(style,"opacity");return ret==""?"1":ret;}if(jQuery.browser.opera&&name=="display"){var save=style.outline;style.outline="0 solid black";style.outline=save;}if(name.match(/float/i))name=styleFloat;if(!force&&style&&style[name])ret=style[name];else if(defaultView.getComputedStyle){if(name.match(/float/i))name="float";name=name.replace(/([A-Z])/g,"-$1").toLowerCase();var computedStyle=defaultView.getComputedStyle(elem,null);if(computedStyle&&!color(elem))ret=computedStyle.getPropertyValue(name);else{var swap=[],stack=[],a=elem,i=0;for(;a&&color(a);a=a.parentNode)stack.unshift(a);for(;i<stack.length;i++)if(color(stack[i])){swap[i]=stack[i].style.display;stack[i].style.display="block";}ret=name=="display"&&swap[stack.length-1]!=null?"none":(computedStyle&&computedStyle.getPropertyValue(name))||"";for(i=0;i<swap.length;i++)if(swap[i]!=null)stack[i].style.display=swap[i];}if(name=="opacity"&&ret=="")ret="1";}else if(elem.currentStyle){var camelCase=name.replace(/\-(\w)/g,function(all,letter){return letter.toUpperCase();});ret=elem.currentStyle[name]||elem.currentStyle[camelCase];if(!/^\d+(px)?$/i.test(ret)&&/^\d/.test(ret)){var left=style.left,rsLeft=elem.runtimeStyle.left;elem.runtimeStyle.left=elem.currentStyle.left;style.left=ret||0;ret=style.pixelLeft+"px";style.left=left;elem.runtimeStyle.left=rsLeft;}}return ret;},clean:function(elems,context){var ret=[];context=context||document;if(typeof context.createElement=='undefined')context=context.ownerDocument||context[0]&&context[0].ownerDocument||document;jQuery.each(elems,function(i,elem){if(!elem)return;if(elem.constructor==Number)elem+='';if(typeof elem=="string"){elem=elem.replace(/(<(\w+)[^>]*?)\/>/g,function(all,front,tag){return tag.match(/^(abbr|br|col|img|input|link|meta|param|hr|area|embed)$/i)?all:front+"></"+tag+">";});var tags=jQuery.trim(elem).toLowerCase(),div=context.createElement("div");var wrap=!tags.indexOf("<opt")&&[1,"<select multiple='multiple'>","</select>"]||!tags.indexOf("<leg")&&[1,"<fieldset>","</fieldset>"]||tags.match(/^<(thead|tbody|tfoot|colg|cap)/)&&[1,"<table>","</table>"]||!tags.indexOf("<tr")&&[2,"<table><tbody>","</tbody></table>"]||(!tags.indexOf("<td")||!tags.indexOf("<th"))&&[3,"<table><tbody><tr>","</tr></tbody></table>"]||!tags.indexOf("<col")&&[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"]||jQuery.browser.msie&&[1,"div<div>","</div>"]||[0,"",""];div.innerHTML=wrap[1]+elem+wrap[2];while(wrap[0]--)div=div.lastChild;if(jQuery.browser.msie){var tbody=!tags.indexOf("<table")&&tags.indexOf("<tbody")<0?div.firstChild&&div.firstChild.childNodes:wrap[1]=="<table>"&&tags.indexOf("<tbody")<0?div.childNodes:[];for(var j=tbody.length-1;j>=0;--j)if(jQuery.nodeName(tbody[j],"tbody")&&!tbody[j].childNodes.length)tbody[j].parentNode.removeChild(tbody[j]);if(/^\s/.test(elem))div.insertBefore(context.createTextNode(elem.match(/^\s*/)[0]),div.firstChild);}elem=jQuery.makeArray(div.childNodes);}if(elem.length===0&&(!jQuery.nodeName(elem,"form")&&!jQuery.nodeName(elem,"select")))return;if(elem[0]==undefined||jQuery.nodeName(elem,"form")||elem.options)ret.push(elem);else
-ret=jQuery.merge(ret,elem);});return ret;},attr:function(elem,name,value){if(!elem||elem.nodeType==3||elem.nodeType==8)return undefined;var notxml=!jQuery.isXMLDoc(elem),set=value!==undefined,msie=jQuery.browser.msie;name=notxml&&jQuery.props[name]||name;if(elem.tagName){var special=/href|src|style/.test(name);if(name=="selected"&&jQuery.browser.safari)elem.parentNode.selectedIndex;if(name in elem&&notxml&&!special){if(set){if(name=="type"&&jQuery.nodeName(elem,"input")&&elem.parentNode)throw"type property can't be changed";elem[name]=value;}if(jQuery.nodeName(elem,"form")&&elem.getAttributeNode(name))return elem.getAttributeNode(name).nodeValue;return elem[name];}if(msie&&notxml&&name=="style")return jQuery.attr(elem.style,"cssText",value);if(set)elem.setAttribute(name,""+value);var attr=msie&&notxml&&special?elem.getAttribute(name,2):elem.getAttribute(name);return attr===null?undefined:attr;}if(msie&&name=="opacity"){if(set){elem.zoom=1;elem.filter=(elem.filter||"").replace(/alpha\([^)]*\)/,"")+(parseInt(value)+''=="NaN"?"":"alpha(opacity="+value*100+")");}return elem.filter&&elem.filter.indexOf("opacity=")>=0?(parseFloat(elem.filter.match(/opacity=([^)]*)/)[1])/100)+'':"";}name=name.replace(/-([a-z])/ig,function(all,letter){return letter.toUpperCase();});if(set)elem[name]=value;return elem[name];},trim:function(text){return(text||"").replace(/^\s+|\s+$/g,"");},makeArray:function(array){var ret=[];if(array!=null){var i=array.length;if(i==null||array.split||array.setInterval||array.call)ret[0]=array;else
-while(i)ret[--i]=array[i];}return ret;},inArray:function(elem,array){for(var i=0,length=array.length;i<length;i++)if(array[i]===elem)return i;return-1;},merge:function(first,second){var i=0,elem,pos=first.length;if(jQuery.browser.msie){while(elem=second[i++])if(elem.nodeType!=8)first[pos++]=elem;}else
-while(elem=second[i++])first[pos++]=elem;return first;},unique:function(array){var ret=[],done={};try{for(var i=0,length=array.length;i<length;i++){var id=jQuery.data(array[i]);if(!done[id]){done[id]=true;ret.push(array[i]);}}}catch(e){ret=array;}return ret;},grep:function(elems,callback,inv){var ret=[];for(var i=0,length=elems.length;i<length;i++)if(!inv!=!callback(elems[i],i))ret.push(elems[i]);return ret;},map:function(elems,callback){var ret=[];for(var i=0,length=elems.length;i<length;i++){var value=callback(elems[i],i);if(value!=null)ret[ret.length]=value;}return ret.concat.apply([],ret);}});var userAgent=navigator.userAgent.toLowerCase();jQuery.browser={version:(userAgent.match(/.+(?:rv|it|ra|ie)[\/: ]([\d.]+)/)||[])[1],safari:/webkit/.test(userAgent),opera:/opera/.test(userAgent),msie:/msie/.test(userAgent)&&!/opera/.test(userAgent),mozilla:/mozilla/.test(userAgent)&&!/(compatible|webkit)/.test(userAgent)};var styleFloat=jQuery.browser.msie?"styleFloat":"cssFloat";jQuery.extend({boxModel:!jQuery.browser.msie||document.compatMode=="CSS1Compat",props:{"for":"htmlFor","class":"className","float":styleFloat,cssFloat:styleFloat,styleFloat:styleFloat,readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing"}});jQuery.each({parent:function(elem){return elem.parentNode;},parents:function(elem){return jQuery.dir(elem,"parentNode");},next:function(elem){return jQuery.nth(elem,2,"nextSibling");},prev:function(elem){return jQuery.nth(elem,2,"previousSibling");},nextAll:function(elem){return jQuery.dir(elem,"nextSibling");},prevAll:function(elem){return jQuery.dir(elem,"previousSibling");},siblings:function(elem){return jQuery.sibling(elem.parentNode.firstChild,elem);},children:function(elem){return jQuery.sibling(elem.firstChild);},contents:function(elem){return jQuery.nodeName(elem,"iframe")?elem.contentDocument||elem.contentWindow.document:jQuery.makeArray(elem.childNodes);}},function(name,fn){jQuery.fn[name]=function(selector){var ret=jQuery.map(this,fn);if(selector&&typeof selector=="string")ret=jQuery.multiFilter(selector,ret);return this.pushStack(jQuery.unique(ret));};});jQuery.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(name,original){jQuery.fn[name]=function(){var args=arguments;return this.each(function(){for(var i=0,length=args.length;i<length;i++)jQuery(args[i])[original](this);});};});jQuery.each({removeAttr:function(name){jQuery.attr(this,name,"");if(this.nodeType==1)this.removeAttribute(name);},addClass:function(classNames){jQuery.className.add(this,classNames);},removeClass:function(classNames){jQuery.className.remove(this,classNames);},toggleClass:function(classNames){jQuery.className[jQuery.className.has(this,classNames)?"remove":"add"](this,classNames);},remove:function(selector){if(!selector||jQuery.filter(selector,[this]).r.length){jQuery("*",this).add(this).each(function(){jQuery.event.remove(this);jQuery.removeData(this);});if(this.parentNode)this.parentNode.removeChild(this);}},empty:function(){jQuery(">*",this).remove();while(this.firstChild)this.removeChild(this.firstChild);}},function(name,fn){jQuery.fn[name]=function(){return this.each(fn,arguments);};});jQuery.each(["Height","Width"],function(i,name){var type=name.toLowerCase();jQuery.fn[type]=function(size){return this[0]==window?jQuery.browser.opera&&document.body["client"+name]||jQuery.browser.safari&&window["inner"+name]||document.compatMode=="CSS1Compat"&&document.documentElement["client"+name]||document.body["client"+name]:this[0]==document?Math.max(Math.max(document.body["scroll"+name],document.documentElement["scroll"+name]),Math.max(document.body["offset"+name],document.documentElement["offset"+name])):size==undefined?(this.length?jQuery.css(this[0],type):null):this.css(type,size.constructor==String?size:size+"px");};});function num(elem,prop){return elem[0]&&parseInt(jQuery.curCSS(elem[0],prop,true),10)||0;}var chars=jQuery.browser.safari&&parseInt(jQuery.browser.version)<417?"(?:[\\w*_-]|\\\\.)":"(?:[\\w\u0128-\uFFFF*_-]|\\\\.)",quickChild=new RegExp("^>\\s*("+chars+"+)"),quickID=new RegExp("^("+chars+"+)(#)("+chars+"+)"),quickClass=new RegExp("^([#.]?)("+chars+"*)");jQuery.extend({expr:{"":function(a,i,m){return m[2]=="*"||jQuery.nodeName(a,m[2]);},"#":function(a,i,m){return a.getAttribute("id")==m[2];},":":{lt:function(a,i,m){return i<m[3]-0;},gt:function(a,i,m){return i>m[3]-0;},nth:function(a,i,m){return m[3]-0==i;},eq:function(a,i,m){return m[3]-0==i;},first:function(a,i){return i==0;},last:function(a,i,m,r){return i==r.length-1;},even:function(a,i){return i%2==0;},odd:function(a,i){return i%2;},"first-child":function(a){return a.parentNode.getElementsByTagName("*")[0]==a;},"last-child":function(a){return jQuery.nth(a.parentNode.lastChild,1,"previousSibling")==a;},"only-child":function(a){return!jQuery.nth(a.parentNode.lastChild,2,"previousSibling");},parent:function(a){return a.firstChild;},empty:function(a){return!a.firstChild;},contains:function(a,i,m){return(a.textContent||a.innerText||jQuery(a).text()||"").indexOf(m[3])>=0;},visible:function(a){return"hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden";},hidden:function(a){return"hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden";},enabled:function(a){return!a.disabled;},disabled:function(a){return a.disabled;},checked:function(a){return a.checked;},selected:function(a){return a.selected||jQuery.attr(a,"selected");},text:function(a){return"text"==a.type;},radio:function(a){return"radio"==a.type;},checkbox:function(a){return"checkbox"==a.type;},file:function(a){return"file"==a.type;},password:function(a){return"password"==a.type;},submit:function(a){return"submit"==a.type;},image:function(a){return"image"==a.type;},reset:function(a){return"reset"==a.type;},button:function(a){return"button"==a.type||jQuery.nodeName(a,"button");},input:function(a){return/input|select|textarea|button/i.test(a.nodeName);},has:function(a,i,m){return jQuery.find(m[3],a).length;},header:function(a){return/h\d/i.test(a.nodeName);},animated:function(a){return jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length;}}},parse:[/^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,/^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,new RegExp("^([:.#]*)("+chars+"+)")],multiFilter:function(expr,elems,not){var old,cur=[];while(expr&&expr!=old){old=expr;var f=jQuery.filter(expr,elems,not);expr=f.t.replace(/^\s*,\s*/,"");cur=not?elems=f.r:jQuery.merge(cur,f.r);}return cur;},find:function(t,context){if(typeof t!="string")return[t];if(context&&context.nodeType!=1&&context.nodeType!=9)return[];context=context||document;var ret=[context],done=[],last,nodeName;while(t&&last!=t){var r=[];last=t;t=jQuery.trim(t);var foundToken=false,re=quickChild,m=re.exec(t);if(m){nodeName=m[1].toUpperCase();for(var i=0;ret[i];i++)for(var c=ret[i].firstChild;c;c=c.nextSibling)if(c.nodeType==1&&(nodeName=="*"||c.nodeName.toUpperCase()==nodeName))r.push(c);ret=r;t=t.replace(re,"");if(t.indexOf(" ")==0)continue;foundToken=true;}else{re=/^([>+~])\s*(\w*)/i;if((m=re.exec(t))!=null){r=[];var merge={};nodeName=m[2].toUpperCase();m=m[1];for(var j=0,rl=ret.length;j<rl;j++){var n=m=="~"||m=="+"?ret[j].nextSibling:ret[j].firstChild;for(;n;n=n.nextSibling)if(n.nodeType==1){var id=jQuery.data(n);if(m=="~"&&merge[id])break;if(!nodeName||n.nodeName.toUpperCase()==nodeName){if(m=="~")merge[id]=true;r.push(n);}if(m=="+")break;}}ret=r;t=jQuery.trim(t.replace(re,""));foundToken=true;}}if(t&&!foundToken){if(!t.indexOf(",")){if(context==ret[0])ret.shift();done=jQuery.merge(done,ret);r=ret=[context];t=" "+t.substr(1,t.length);}else{var re2=quickID;var m=re2.exec(t);if(m){m=[0,m[2],m[3],m[1]];}else{re2=quickClass;m=re2.exec(t);}m[2]=m[2].replace(/\\/g,"");var elem=ret[ret.length-1];if(m[1]=="#"&&elem&&elem.getElementById&&!jQuery.isXMLDoc(elem)){var oid=elem.getElementById(m[2]);if((jQuery.browser.msie||jQuery.browser.opera)&&oid&&typeof oid.id=="string"&&oid.id!=m[2])oid=jQuery('[@id="'+m[2]+'"]',elem)[0];ret=r=oid&&(!m[3]||jQuery.nodeName(oid,m[3]))?[oid]:[];}else{for(var i=0;ret[i];i++){var tag=m[1]=="#"&&m[3]?m[3]:m[1]!=""||m[0]==""?"*":m[2];if(tag=="*"&&ret[i].nodeName.toLowerCase()=="object")tag="param";r=jQuery.merge(r,ret[i].getElementsByTagName(tag));}if(m[1]==".")r=jQuery.classFilter(r,m[2]);if(m[1]=="#"){var tmp=[];for(var i=0;r[i];i++)if(r[i].getAttribute("id")==m[2]){tmp=[r[i]];break;}r=tmp;}ret=r;}t=t.replace(re2,"");}}if(t){var val=jQuery.filter(t,r);ret=r=val.r;t=jQuery.trim(val.t);}}if(t)ret=[];if(ret&&context==ret[0])ret.shift();done=jQuery.merge(done,ret);return done;},classFilter:function(r,m,not){m=" "+m+" ";var tmp=[];for(var i=0;r[i];i++){var pass=(" "+r[i].className+" ").indexOf(m)>=0;if(!not&&pass||not&&!pass)tmp.push(r[i]);}return tmp;},filter:function(t,r,not){var last;while(t&&t!=last){last=t;var p=jQuery.parse,m;for(var i=0;p[i];i++){m=p[i].exec(t);if(m){t=t.substring(m[0].length);m[2]=m[2].replace(/\\/g,"");break;}}if(!m)break;if(m[1]==":"&&m[2]=="not")r=isSimple.test(m[3])?jQuery.filter(m[3],r,true).r:jQuery(r).not(m[3]);else if(m[1]==".")r=jQuery.classFilter(r,m[2],not);else if(m[1]=="["){var tmp=[],type=m[3];for(var i=0,rl=r.length;i<rl;i++){var a=r[i],z=a[jQuery.props[m[2]]||m[2]];if(z==null||/href|src|selected/.test(m[2]))z=jQuery.attr(a,m[2])||'';if((type==""&&!!z||type=="="&&z==m[5]||type=="!="&&z!=m[5]||type=="^="&&z&&!z.indexOf(m[5])||type=="$="&&z.substr(z.length-m[5].length)==m[5]||(type=="*="||type=="~=")&&z.indexOf(m[5])>=0)^not)tmp.push(a);}r=tmp;}else if(m[1]==":"&&m[2]=="nth-child"){var merge={},tmp=[],test=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(m[3]=="even"&&"2n"||m[3]=="odd"&&"2n+1"||!/\D/.test(m[3])&&"0n+"+m[3]||m[3]),first=(test[1]+(test[2]||1))-0,last=test[3]-0;for(var i=0,rl=r.length;i<rl;i++){var node=r[i],parentNode=node.parentNode,id=jQuery.data(parentNode);if(!merge[id]){var c=1;for(var n=parentNode.firstChild;n;n=n.nextSibling)if(n.nodeType==1)n.nodeIndex=c++;merge[id]=true;}var add=false;if(first==0){if(node.nodeIndex==last)add=true;}else if((node.nodeIndex-last)%first==0&&(node.nodeIndex-last)/first>=0)add=true;if(add^not)tmp.push(node);}r=tmp;}else{var fn=jQuery.expr[m[1]];if(typeof fn=="object")fn=fn[m[2]];if(typeof fn=="string")fn=eval("false||function(a,i){return "+fn+";}");r=jQuery.grep(r,function(elem,i){return fn(elem,i,m,r);},not);}}return{r:r,t:t};},dir:function(elem,dir){var matched=[],cur=elem[dir];while(cur&&cur!=document){if(cur.nodeType==1)matched.push(cur);cur=cur[dir];}return matched;},nth:function(cur,result,dir,elem){result=result||1;var num=0;for(;cur;cur=cur[dir])if(cur.nodeType==1&&++num==result)break;return cur;},sibling:function(n,elem){var r=[];for(;n;n=n.nextSibling){if(n.nodeType==1&&n!=elem)r.push(n);}return r;}});jQuery.event={add:function(elem,types,handler,data){if(elem.nodeType==3||elem.nodeType==8)return;if(jQuery.browser.msie&&elem.setInterval)elem=window;if(!handler.guid)handler.guid=this.guid++;if(data!=undefined){var fn=handler;handler=this.proxy(fn,function(){return fn.apply(this,arguments);});handler.data=data;}var events=jQuery.data(elem,"events")||jQuery.data(elem,"events",{}),handle=jQuery.data(elem,"handle")||jQuery.data(elem,"handle",function(){if(typeof jQuery!="undefined"&&!jQuery.event.triggered)return jQuery.event.handle.apply(arguments.callee.elem,arguments);});handle.elem=elem;jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];handler.type=parts[1];var handlers=events[type];if(!handlers){handlers=events[type]={};if(!jQuery.event.special[type]||jQuery.event.special[type].setup.call(elem)===false){if(elem.addEventListener)elem.addEventListener(type,handle,false);else if(elem.attachEvent)elem.attachEvent("on"+type,handle);}}handlers[handler.guid]=handler;jQuery.event.global[type]=true;});elem=null;},guid:1,global:{},remove:function(elem,types,handler){if(elem.nodeType==3||elem.nodeType==8)return;var events=jQuery.data(elem,"events"),ret,index;if(events){if(types==undefined||(typeof types=="string"&&types.charAt(0)=="."))for(var type in events)this.remove(elem,type+(types||""));else{if(types.type){handler=types.handler;types=types.type;}jQuery.each(types.split(/\s+/),function(index,type){var parts=type.split(".");type=parts[0];if(events[type]){if(handler)delete events[type][handler.guid];else
-for(handler in events[type])if(!parts[1]||events[type][handler].type==parts[1])delete events[type][handler];for(ret in events[type])break;if(!ret){if(!jQuery.event.special[type]||jQuery.event.special[type].teardown.call(elem)===false){if(elem.removeEventListener)elem.removeEventListener(type,jQuery.data(elem,"handle"),false);else if(elem.detachEvent)elem.detachEvent("on"+type,jQuery.data(elem,"handle"));}ret=null;delete events[type];}}});}for(ret in events)break;if(!ret){var handle=jQuery.data(elem,"handle");if(handle)handle.elem=null;jQuery.removeData(elem,"events");jQuery.removeData(elem,"handle");}}},trigger:function(type,data,elem,donative,extra){data=jQuery.makeArray(data);if(type.indexOf("!")>=0){type=type.slice(0,-1);var exclusive=true;}if(!elem){if(this.global[type])jQuery("*").add([window,document]).trigger(type,data);}else{if(elem.nodeType==3||elem.nodeType==8)return undefined;var val,ret,fn=jQuery.isFunction(elem[type]||null),event=!data[0]||!data[0].preventDefault;if(event){data.unshift({type:type,target:elem,preventDefault:function(){},stopPropagation:function(){},timeStamp:now()});data[0][expando]=true;}data[0].type=type;if(exclusive)data[0].exclusive=true;var handle=jQuery.data(elem,"handle");if(handle)val=handle.apply(elem,data);if((!fn||(jQuery.nodeName(elem,'a')&&type=="click"))&&elem["on"+type]&&elem["on"+type].apply(elem,data)===false)val=false;if(event)data.shift();if(extra&&jQuery.isFunction(extra)){ret=extra.apply(elem,val==null?data:data.concat(val));if(ret!==undefined)val=ret;}if(fn&&donative!==false&&val!==false&&!(jQuery.nodeName(elem,'a')&&type=="click")){this.triggered=true;try{elem[type]();}catch(e){}}this.triggered=false;}return val;},handle:function(event){var val,ret,namespace,all,handlers;event=arguments[0]=jQuery.event.fix(event||window.event);namespace=event.type.split(".");event.type=namespace[0];namespace=namespace[1];all=!namespace&&!event.exclusive;handlers=(jQuery.data(this,"events")||{})[event.type];for(var j in handlers){var handler=handlers[j];if(all||handler.type==namespace){event.handler=handler;event.data=handler.data;ret=handler.apply(this,arguments);if(val!==false)val=ret;if(ret===false){event.preventDefault();event.stopPropagation();}}}return val;},fix:function(event){if(event[expando]==true)return event;var originalEvent=event;event={originalEvent:originalEvent};var props="altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode metaKey newValue originalTarget pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target timeStamp toElement type view wheelDelta which".split(" ");for(var i=props.length;i;i--)event[props[i]]=originalEvent[props[i]];event[expando]=true;event.preventDefault=function(){if(originalEvent.preventDefault)originalEvent.preventDefault();originalEvent.returnValue=false;};event.stopPropagation=function(){if(originalEvent.stopPropagation)originalEvent.stopPropagation();originalEvent.cancelBubble=true;};event.timeStamp=event.timeStamp||now();if(!event.target)event.target=event.srcElement||document;if(event.target.nodeType==3)event.target=event.target.parentNode;if(!event.relatedTarget&&event.fromElement)event.relatedTarget=event.fromElement==event.target?event.toElement:event.fromElement;if(event.pageX==null&&event.clientX!=null){var doc=document.documentElement,body=document.body;event.pageX=event.clientX+(doc&&doc.scrollLeft||body&&body.scrollLeft||0)-(doc.clientLeft||0);event.pageY=event.clientY+(doc&&doc.scrollTop||body&&body.scrollTop||0)-(doc.clientTop||0);}if(!event.which&&((event.charCode||event.charCode===0)?event.charCode:event.keyCode))event.which=event.charCode||event.keyCode;if(!event.metaKey&&event.ctrlKey)event.metaKey=event.ctrlKey;if(!event.which&&event.button)event.which=(event.button&1?1:(event.button&2?3:(event.button&4?2:0)));return event;},proxy:function(fn,proxy){proxy.guid=fn.guid=fn.guid||proxy.guid||this.guid++;return proxy;},special:{ready:{setup:function(){bindReady();return;},teardown:function(){return;}},mouseenter:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseover",jQuery.event.special.mouseenter.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseover",jQuery.event.special.mouseenter.handler);return true;},handler:function(event){if(withinElement(event,this))return true;event.type="mouseenter";return jQuery.event.handle.apply(this,arguments);}},mouseleave:{setup:function(){if(jQuery.browser.msie)return false;jQuery(this).bind("mouseout",jQuery.event.special.mouseleave.handler);return true;},teardown:function(){if(jQuery.browser.msie)return false;jQuery(this).unbind("mouseout",jQuery.event.special.mouseleave.handler);return true;},handler:function(event){if(withinElement(event,this))return true;event.type="mouseleave";return jQuery.event.handle.apply(this,arguments);}}}};jQuery.fn.extend({bind:function(type,data,fn){return type=="unload"?this.one(type,data,fn):this.each(function(){jQuery.event.add(this,type,fn||data,fn&&data);});},one:function(type,data,fn){var one=jQuery.event.proxy(fn||data,function(event){jQuery(this).unbind(event,one);return(fn||data).apply(this,arguments);});return this.each(function(){jQuery.event.add(this,type,one,fn&&data);});},unbind:function(type,fn){return this.each(function(){jQuery.event.remove(this,type,fn);});},trigger:function(type,data,fn){return this.each(function(){jQuery.event.trigger(type,data,this,true,fn);});},triggerHandler:function(type,data,fn){return this[0]&&jQuery.event.trigger(type,data,this[0],false,fn);},toggle:function(fn){var args=arguments,i=1;while(i<args.length)jQuery.event.proxy(fn,args[i++]);return this.click(jQuery.event.proxy(fn,function(event){this.lastToggle=(this.lastToggle||0)%i;event.preventDefault();return args[this.lastToggle++].apply(this,arguments)||false;}));},hover:function(fnOver,fnOut){return this.bind('mouseenter',fnOver).bind('mouseleave',fnOut);},ready:function(fn){bindReady();if(jQuery.isReady)fn.call(document,jQuery);else
-jQuery.readyList.push(function(){return fn.call(this,jQuery);});return this;}});jQuery.extend({isReady:false,readyList:[],ready:function(){if(!jQuery.isReady){jQuery.isReady=true;if(jQuery.readyList){jQuery.each(jQuery.readyList,function(){this.call(document);});jQuery.readyList=null;}jQuery(document).triggerHandler("ready");}}});var readyBound=false;function bindReady(){if(readyBound)return;readyBound=true;if(document.addEventListener&&!jQuery.browser.opera)document.addEventListener("DOMContentLoaded",jQuery.ready,false);if(jQuery.browser.msie&&window==top)(function(){if(jQuery.isReady)return;try{document.documentElement.doScroll("left");}catch(error){setTimeout(arguments.callee,0);return;}jQuery.ready();})();if(jQuery.browser.opera)document.addEventListener("DOMContentLoaded",function(){if(jQuery.isReady)return;for(var i=0;i<document.styleSheets.length;i++)if(document.styleSheets[i].disabled){setTimeout(arguments.callee,0);return;}jQuery.ready();},false);if(jQuery.browser.safari){var numStyles;(function(){if(jQuery.isReady)return;if(document.readyState!="loaded"&&document.readyState!="complete"){setTimeout(arguments.callee,0);return;}if(numStyles===undefined)numStyles=jQuery("style, link[rel=stylesheet]").length;if(document.styleSheets.length!=numStyles){setTimeout(arguments.callee,0);return;}jQuery.ready();})();}jQuery.event.add(window,"load",jQuery.ready);}jQuery.each(("blur,focus,load,resize,scroll,unload,click,dblclick,"+"mousedown,mouseup,mousemove,mouseover,mouseout,change,select,"+"submit,keydown,keypress,keyup,error").split(","),function(i,name){jQuery.fn[name]=function(fn){return fn?this.bind(name,fn):this.trigger(name);};});var withinElement=function(event,elem){var parent=event.relatedTarget;while(parent&&parent!=elem)try{parent=parent.parentNode;}catch(error){parent=elem;}return parent==elem;};jQuery(window).bind("unload",function(){jQuery("*").add(document).unbind();});jQuery.fn.extend({_load:jQuery.fn.load,load:function(url,params,callback){if(typeof url!='string')return this._load(url);var off=url.indexOf(" ");if(off>=0){var selector=url.slice(off,url.length);url=url.slice(0,off);}callback=callback||function(){};var type="GET";if(params)if(jQuery.isFunction(params)){callback=params;params=null;}else{params=jQuery.param(params);type="POST";}var self=this;jQuery.ajax({url:url,type:type,dataType:"html",data:params,complete:function(res,status){if(status=="success"||status=="notmodified")self.html(selector?jQuery("<div/>").append(res.responseText.replace(/<script(.|\s)*?\/script>/g,"")).find(selector):res.responseText);self.each(callback,[res.responseText,status,res]);}});return this;},serialize:function(){return jQuery.param(this.serializeArray());},serializeArray:function(){return this.map(function(){return jQuery.nodeName(this,"form")?jQuery.makeArray(this.elements):this;}).filter(function(){return this.name&&!this.disabled&&(this.checked||/select|textarea/i.test(this.nodeName)||/text|hidden|password/i.test(this.type));}).map(function(i,elem){var val=jQuery(this).val();return val==null?null:val.constructor==Array?jQuery.map(val,function(val,i){return{name:elem.name,value:val};}):{name:elem.name,value:val};}).get();}});jQuery.each("ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","),function(i,o){jQuery.fn[o]=function(f){return this.bind(o,f);};});var jsc=now();jQuery.extend({get:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data=null;}return jQuery.ajax({type:"GET",url:url,data:data,success:callback,dataType:type});},getScript:function(url,callback){return jQuery.get(url,null,callback,"script");},getJSON:function(url,data,callback){return jQuery.get(url,data,callback,"json");},post:function(url,data,callback,type){if(jQuery.isFunction(data)){callback=data;data={};}return jQuery.ajax({type:"POST",url:url,data:data,success:callback,dataType:type});},ajaxSetup:function(settings){jQuery.extend(jQuery.ajaxSettings,settings);},ajaxSettings:{url:location.href,global:true,type:"GET",timeout:0,contentType:"application/x-www-form-urlencoded",processData:true,async:true,data:null,username:null,password:null,accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},ajax:function(s){s=jQuery.extend(true,s,jQuery.extend(true,{},jQuery.ajaxSettings,s));var jsonp,jsre=/=\?(&|$)/g,status,data,type=s.type.toUpperCase();if(s.data&&s.processData&&typeof s.data!="string")s.data=jQuery.param(s.data);if(s.dataType=="jsonp"){if(type=="GET"){if(!s.url.match(jsre))s.url+=(s.url.match(/\?/)?"&":"?")+(s.jsonp||"callback")+"=?";}else if(!s.data||!s.data.match(jsre))s.data=(s.data?s.data+"&":"")+(s.jsonp||"callback")+"=?";s.dataType="json";}if(s.dataType=="json"&&(s.data&&s.data.match(jsre)||s.url.match(jsre))){jsonp="jsonp"+jsc++;if(s.data)s.data=(s.data+"").replace(jsre,"="+jsonp+"$1");s.url=s.url.replace(jsre,"="+jsonp+"$1");s.dataType="script";window[jsonp]=function(tmp){data=tmp;success();complete();window[jsonp]=undefined;try{delete window[jsonp];}catch(e){}if(head)head.removeChild(script);};}if(s.dataType=="script"&&s.cache==null)s.cache=false;if(s.cache===false&&type=="GET"){var ts=now();var ret=s.url.replace(/(\?|&)_=.*?(&|$)/,"$1_="+ts+"$2");s.url=ret+((ret==s.url)?(s.url.match(/\?/)?"&":"?")+"_="+ts:"");}if(s.data&&type=="GET"){s.url+=(s.url.match(/\?/)?"&":"?")+s.data;s.data=null;}if(s.global&&!jQuery.active++)jQuery.event.trigger("ajaxStart");var remote=/^(?:\w+:)?\/\/([^\/?#]+)/;if(s.dataType=="script"&&type=="GET"&&remote.test(s.url)&&remote.exec(s.url)[1]!=location.host){var head=document.getElementsByTagName("head")[0];var script=document.createElement("script");script.src=s.url;if(s.scriptCharset)script.charset=s.scriptCharset;if(!jsonp){var done=false;script.onload=script.onreadystatechange=function(){if(!done&&(!this.readyState||this.readyState=="loaded"||this.readyState=="complete")){done=true;success();complete();head.removeChild(script);}};}head.appendChild(script);return undefined;}var requestDone=false;var xhr=window.ActiveXObject?new ActiveXObject("Microsoft.XMLHTTP"):new XMLHttpRequest();if(s.username)xhr.open(type,s.url,s.async,s.username,s.password);else
-xhr.open(type,s.url,s.async);try{if(s.data)xhr.setRequestHeader("Content-Type",s.contentType);if(s.ifModified)xhr.setRequestHeader("If-Modified-Since",jQuery.lastModified[s.url]||"Thu, 01 Jan 1970 00:00:00 GMT");xhr.setRequestHeader("X-Requested-With","XMLHttpRequest");xhr.setRequestHeader("Accept",s.dataType&&s.accepts[s.dataType]?s.accepts[s.dataType]+", */*":s.accepts._default);}catch(e){}if(s.beforeSend&&s.beforeSend(xhr,s)===false){s.global&&jQuery.active--;xhr.abort();return false;}if(s.global)jQuery.event.trigger("ajaxSend",[xhr,s]);var onreadystatechange=function(isTimeout){if(!requestDone&&xhr&&(xhr.readyState==4||isTimeout=="timeout")){requestDone=true;if(ival){clearInterval(ival);ival=null;}status=isTimeout=="timeout"&&"timeout"||!jQuery.httpSuccess(xhr)&&"error"||s.ifModified&&jQuery.httpNotModified(xhr,s.url)&&"notmodified"||"success";if(status=="success"){try{data=jQuery.httpData(xhr,s.dataType,s.dataFilter);}catch(e){status="parsererror";}}if(status=="success"){var modRes;try{modRes=xhr.getResponseHeader("Last-Modified");}catch(e){}if(s.ifModified&&modRes)jQuery.lastModified[s.url]=modRes;if(!jsonp)success();}else
-jQuery.handleError(s,xhr,status);complete();if(s.async)xhr=null;}};if(s.async){var ival=setInterval(onreadystatechange,13);if(s.timeout>0)setTimeout(function(){if(xhr){xhr.abort();if(!requestDone)onreadystatechange("timeout");}},s.timeout);}try{xhr.send(s.data);}catch(e){jQuery.handleError(s,xhr,null,e);}if(!s.async)onreadystatechange();function success(){if(s.success)s.success(data,status);if(s.global)jQuery.event.trigger("ajaxSuccess",[xhr,s]);}function complete(){if(s.complete)s.complete(xhr,status);if(s.global)jQuery.event.trigger("ajaxComplete",[xhr,s]);if(s.global&&!--jQuery.active)jQuery.event.trigger("ajaxStop");}return xhr;},handleError:function(s,xhr,status,e){if(s.error)s.error(xhr,status,e);if(s.global)jQuery.event.trigger("ajaxError",[xhr,s,e]);},active:0,httpSuccess:function(xhr){try{return!xhr.status&&location.protocol=="file:"||(xhr.status>=200&&xhr.status<300)||xhr.status==304||xhr.status==1223||jQuery.browser.safari&&xhr.status==undefined;}catch(e){}return false;},httpNotModified:function(xhr,url){try{var xhrRes=xhr.getResponseHeader("Last-Modified");return xhr.status==304||xhrRes==jQuery.lastModified[url]||jQuery.browser.safari&&xhr.status==undefined;}catch(e){}return false;},httpData:function(xhr,type,filter){var ct=xhr.getResponseHeader("content-type"),xml=type=="xml"||!type&&ct&&ct.indexOf("xml")>=0,data=xml?xhr.responseXML:xhr.responseText;if(xml&&data.documentElement.tagName=="parsererror")throw"parsererror";if(filter)data=filter(data,type);if(type=="script")jQuery.globalEval(data);if(type=="json")data=eval("("+data+")");return data;},param:function(a){var s=[];if(a.constructor==Array||a.jquery)jQuery.each(a,function(){s.push(encodeURIComponent(this.name)+"="+encodeURIComponent(this.value));});else
-for(var j in a)if(a[j]&&a[j].constructor==Array)jQuery.each(a[j],function(){s.push(encodeURIComponent(j)+"="+encodeURIComponent(this));});else
-s.push(encodeURIComponent(j)+"="+encodeURIComponent(jQuery.isFunction(a[j])?a[j]():a[j]));return s.join("&").replace(/%20/g,"+");}});jQuery.fn.extend({show:function(speed,callback){return speed?this.animate({height:"show",width:"show",opacity:"show"},speed,callback):this.filter(":hidden").each(function(){this.style.display=this.oldblock||"";if(jQuery.css(this,"display")=="none"){var elem=jQuery("<"+this.tagName+" />").appendTo("body");this.style.display=elem.css("display");if(this.style.display=="none")this.style.display="block";elem.remove();}}).end();},hide:function(speed,callback){return speed?this.animate({height:"hide",width:"hide",opacity:"hide"},speed,callback):this.filter(":visible").each(function(){this.oldblock=this.oldblock||jQuery.css(this,"display");this.style.display="none";}).end();},_toggle:jQuery.fn.toggle,toggle:function(fn,fn2){return jQuery.isFunction(fn)&&jQuery.isFunction(fn2)?this._toggle.apply(this,arguments):fn?this.animate({height:"toggle",width:"toggle",opacity:"toggle"},fn,fn2):this.each(function(){jQuery(this)[jQuery(this).is(":hidden")?"show":"hide"]();});},slideDown:function(speed,callback){return this.animate({height:"show"},speed,callback);},slideUp:function(speed,callback){return this.animate({height:"hide"},speed,callback);},slideToggle:function(speed,callback){return this.animate({height:"toggle"},speed,callback);},fadeIn:function(speed,callback){return this.animate({opacity:"show"},speed,callback);},fadeOut:function(speed,callback){return this.animate({opacity:"hide"},speed,callback);},fadeTo:function(speed,to,callback){return this.animate({opacity:to},speed,callback);},animate:function(prop,speed,easing,callback){var optall=jQuery.speed(speed,easing,callback);return this[optall.queue===false?"each":"queue"](function(){if(this.nodeType!=1)return false;var opt=jQuery.extend({},optall),p,hidden=jQuery(this).is(":hidden"),self=this;for(p in prop){if(prop[p]=="hide"&&hidden||prop[p]=="show"&&!hidden)return opt.complete.call(this);if(p=="height"||p=="width"){opt.display=jQuery.css(this,"display");opt.overflow=this.style.overflow;}}if(opt.overflow!=null)this.style.overflow="hidden";opt.curAnim=jQuery.extend({},prop);jQuery.each(prop,function(name,val){var e=new jQuery.fx(self,opt,name);if(/toggle|show|hide/.test(val))e[val=="toggle"?hidden?"show":"hide":val](prop);else{var parts=val.toString().match(/^([+-]=)?([\d+-.]+)(.*)$/),start=e.cur(true)||0;if(parts){var end=parseFloat(parts[2]),unit=parts[3]||"px";if(unit!="px"){self.style[name]=(end||1)+unit;start=((end||1)/e.cur(true))*start;self.style[name]=start+unit;}if(parts[1])end=((parts[1]=="-="?-1:1)*end)+start;e.custom(start,end,unit);}else
-e.custom(start,val,"");}});return true;});},queue:function(type,fn){if(jQuery.isFunction(type)||(type&&type.constructor==Array)){fn=type;type="fx";}if(!type||(typeof type=="string"&&!fn))return queue(this[0],type);return this.each(function(){if(fn.constructor==Array)queue(this,type,fn);else{queue(this,type).push(fn);if(queue(this,type).length==1)fn.call(this);}});},stop:function(clearQueue,gotoEnd){var timers=jQuery.timers;if(clearQueue)this.queue([]);this.each(function(){for(var i=timers.length-1;i>=0;i--)if(timers[i].elem==this){if(gotoEnd)timers[i](true);timers.splice(i,1);}});if(!gotoEnd)this.dequeue();return this;}});var queue=function(elem,type,array){if(elem){type=type||"fx";var q=jQuery.data(elem,type+"queue");if(!q||array)q=jQuery.data(elem,type+"queue",jQuery.makeArray(array));}return q;};jQuery.fn.dequeue=function(type){type=type||"fx";return this.each(function(){var q=queue(this,type);q.shift();if(q.length)q[0].call(this);});};jQuery.extend({speed:function(speed,easing,fn){var opt=speed&&speed.constructor==Object?speed:{complete:fn||!fn&&easing||jQuery.isFunction(speed)&&speed,duration:speed,easing:fn&&easing||easing&&easing.constructor!=Function&&easing};opt.duration=(opt.duration&&opt.duration.constructor==Number?opt.duration:jQuery.fx.speeds[opt.duration])||jQuery.fx.speeds.def;opt.old=opt.complete;opt.complete=function(){if(opt.queue!==false)jQuery(this).dequeue();if(jQuery.isFunction(opt.old))opt.old.call(this);};return opt;},easing:{linear:function(p,n,firstNum,diff){return firstNum+diff*p;},swing:function(p,n,firstNum,diff){return((-Math.cos(p*Math.PI)/2)+0.5)*diff+firstNum;}},timers:[],timerId:null,fx:function(elem,options,prop){this.options=options;this.elem=elem;this.prop=prop;if(!options.orig)options.orig={};}});jQuery.fx.prototype={update:function(){if(this.options.step)this.options.step.call(this.elem,this.now,this);(jQuery.fx.step[this.prop]||jQuery.fx.step._default)(this);if(this.prop=="height"||this.prop=="width")this.elem.style.display="block";},cur:function(force){if(this.elem[this.prop]!=null&&this.elem.style[this.prop]==null)return this.elem[this.prop];var r=parseFloat(jQuery.css(this.elem,this.prop,force));return r&&r>-10000?r:parseFloat(jQuery.curCSS(this.elem,this.prop))||0;},custom:function(from,to,unit){this.startTime=now();this.start=from;this.end=to;this.unit=unit||this.unit||"px";this.now=this.start;this.pos=this.state=0;this.update();var self=this;function t(gotoEnd){return self.step(gotoEnd);}t.elem=this.elem;jQuery.timers.push(t);if(jQuery.timerId==null){jQuery.timerId=setInterval(function(){var timers=jQuery.timers;for(var i=0;i<timers.length;i++)if(!timers[i]())timers.splice(i--,1);if(!timers.length){clearInterval(jQuery.timerId);jQuery.timerId=null;}},13);}},show:function(){this.options.orig[this.prop]=jQuery.attr(this.elem.style,this.prop);this.options.show=true;this.custom(0,this.cur());if(this.prop=="width"||this.prop=="height")this.elem.style[this.prop]="1px";jQuery(this.elem).show();},hide:function(){this.options.orig[this.prop]=jQuery.attr(this.elem.style,this.prop);this.options.hide=true;this.custom(this.cur(),0);},step:function(gotoEnd){var t=now();if(gotoEnd||t>this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;var done=true;for(var i in this.options.curAnim)if(this.options.curAnim[i]!==true)done=false;if(done){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;this.elem.style.display=this.options.display;if(jQuery.css(this.elem,"display")=="none")this.elem.style.display="block";}if(this.options.hide)this.elem.style.display="none";if(this.options.hide||this.options.show)for(var p in this.options.curAnim)jQuery.attr(this.elem.style,p,this.options.orig[p]);}if(done)this.options.complete.call(this.elem);return false;}else{var n=t-this.startTime;this.state=n/this.options.duration;this.pos=jQuery.easing[this.options.easing||(jQuery.easing.swing?"swing":"linear")](this.state,n,0,1,this.options.duration);this.now=this.start+((this.end-this.start)*this.pos);this.update();}return true;}};jQuery.extend(jQuery.fx,{speeds:{slow:600,fast:200,def:400},step:{scrollLeft:function(fx){fx.elem.scrollLeft=fx.now;},scrollTop:function(fx){fx.elem.scrollTop=fx.now;},opacity:function(fx){jQuery.attr(fx.elem.style,"opacity",fx.now);},_default:function(fx){fx.elem.style[fx.prop]=fx.now+fx.unit;}}});jQuery.fn.offset=function(){var left=0,top=0,elem=this[0],results;if(elem)with(jQuery.browser){var parent=elem.parentNode,offsetChild=elem,offsetParent=elem.offsetParent,doc=elem.ownerDocument,safari2=safari&&parseInt(version)<522&&!/adobeair/i.test(userAgent),css=jQuery.curCSS,fixed=css(elem,"position")=="fixed";if(elem.getBoundingClientRect){var box=elem.getBoundingClientRect();add(box.left+Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),box.top+Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));add(-doc.documentElement.clientLeft,-doc.documentElement.clientTop);}else{add(elem.offsetLeft,elem.offsetTop);while(offsetParent){add(offsetParent.offsetLeft,offsetParent.offsetTop);if(mozilla&&!/^t(able|d|h)$/i.test(offsetParent.tagName)||safari&&!safari2)border(offsetParent);if(!fixed&&css(offsetParent,"position")=="fixed")fixed=true;offsetChild=/^body$/i.test(offsetParent.tagName)?offsetChild:offsetParent;offsetParent=offsetParent.offsetParent;}while(parent&&parent.tagName&&!/^body|html$/i.test(parent.tagName)){if(!/^inline|table.*$/i.test(css(parent,"display")))add(-parent.scrollLeft,-parent.scrollTop);if(mozilla&&css(parent,"overflow")!="visible")border(parent);parent=parent.parentNode;}if((safari2&&(fixed||css(offsetChild,"position")=="absolute"))||(mozilla&&css(offsetChild,"position")!="absolute"))add(-doc.body.offsetLeft,-doc.body.offsetTop);if(fixed)add(Math.max(doc.documentElement.scrollLeft,doc.body.scrollLeft),Math.max(doc.documentElement.scrollTop,doc.body.scrollTop));}results={top:top,left:left};}function border(elem){add(jQuery.curCSS(elem,"borderLeftWidth",true),jQuery.curCSS(elem,"borderTopWidth",true));}function add(l,t){left+=parseInt(l,10)||0;top+=parseInt(t,10)||0;}return results;};jQuery.fn.extend({position:function(){var left=0,top=0,results;if(this[0]){var offsetParent=this.offsetParent(),offset=this.offset(),parentOffset=/^body|html$/i.test(offsetParent[0].tagName)?{top:0,left:0}:offsetParent.offset();offset.top-=num(this,'marginTop');offset.left-=num(this,'marginLeft');parentOffset.top+=num(offsetParent,'borderTopWidth');parentOffset.left+=num(offsetParent,'borderLeftWidth');results={top:offset.top-parentOffset.top,left:offset.left-parentOffset.left};}return results;},offsetParent:function(){var offsetParent=this[0].offsetParent;while(offsetParent&&(!/^body|html$/i.test(offsetParent.tagName)&&jQuery.css(offsetParent,'position')=='static'))offsetParent=offsetParent.offsetParent;return jQuery(offsetParent);}});jQuery.each(['Left','Top'],function(i,name){var method='scroll'+name;jQuery.fn[method]=function(val){if(!this[0])return;return val!=undefined?this.each(function(){this==window||this==document?window.scrollTo(!i?val:jQuery(window).scrollLeft(),i?val:jQuery(window).scrollTop()):this[method]=val;}):this[0]==window||this[0]==document?self[i?'pageYOffset':'pageXOffset']||jQuery.boxModel&&document.documentElement[method]||document.body[method]:this[0][method];};});jQuery.each(["Height","Width"],function(i,name){var tl=i?"Left":"Top",br=i?"Right":"Bottom";jQuery.fn["inner"+name]=function(){return this[name.toLowerCase()]()+num(this,"padding"+tl)+num(this,"padding"+br);};jQuery.fn["outer"+name]=function(margin){return this["inner"+name]()+num(this,"border"+tl+"Width")+num(this,"border"+br+"Width")+(margin?num(this,"margin"+tl)+num(this,"margin"+br):0);};});})();
diff --git a/js/preview.js b/js/preview.js
deleted file mode 100644
--- a/js/preview.js
+++ /dev/null
@@ -1,9 +0,0 @@
-function updatePreviewPane() {
-    $("#previewpane").hide();
-    $("#previewpane").load("/_preview", { "raw" : $("#editedText").attr("value") });
-    $("#previewpane").fadeIn(1000);
-};
-$(document).ready(function(){
-    $("#previewButton").show();
-  });
-
diff --git a/js/search.js b/js/search.js
deleted file mode 100644
--- a/js/search.js
+++ /dev/null
@@ -1,13 +0,0 @@
-function toggleMatches(obj) {
-  obj.next('.matches').slideToggle(300);
-  if (obj.html() == '[show matches]') {
-      obj.html('[hide matches]');
-    } else {
-      obj.html('[show matches]');
-    };
-  }
-$(function() {
-  $('a.showmatch').attr("onClick", "toggleMatches($(this));");
-  $('pre.matches').hide();
-  $('a.showmatch').show();
-  });
diff --git a/js/uploadForm.js b/js/uploadForm.js
deleted file mode 100644
--- a/js/uploadForm.js
+++ /dev/null
@@ -1,3 +0,0 @@
-$(document).ready(function(){
-    $("#file").change(function () { $("#wikiname").val($(this).val()); });
-  });
diff --git a/plugins/CapitalizeEmphasis.hs b/plugins/CapitalizeEmphasis.hs
new file mode 100644
--- /dev/null
+++ b/plugins/CapitalizeEmphasis.hs
@@ -0,0 +1,20 @@
+module CapitalizeEmphasis (plugin) where
+
+-- This plugin converts emphasized text to ALL CAPS.
+-- Not a very useful feature, but useful as an example
+-- of how to write a plugin.
+
+import Network.Gitit.Interface
+import Data.Char (toUpper)
+
+plugin :: Plugin
+plugin = mkPageTransform capsTransform
+
+capsTransform :: [Inline] -> [Inline]
+capsTransform ((Emph x):xs) = processWith capStr x ++ capsTransform xs
+capsTransform (x:xs)        = x : capsTransform xs
+capsTransform []            = []
+
+capStr :: Inline -> Inline
+capStr (Str x) = Str (map toUpper x)
+capStr x       = x
diff --git a/plugins/Deprofanizer.hs b/plugins/Deprofanizer.hs
new file mode 100644
--- /dev/null
+++ b/plugins/Deprofanizer.hs
@@ -0,0 +1,18 @@
+module Deprofanizer (plugin) where
+
+-- This plugin replaces profane words with "XXXXX".
+
+import Network.Gitit.Interface
+import Data.Char (toLower)
+
+plugin :: Plugin
+plugin = mkPageTransform deprofanize
+
+deprofanize :: Inline -> Inline
+deprofanize (Str x) | isBadWord x = Str "XXXXX"
+deprofanize x                     = x
+
+isBadWord :: String -> Bool
+isBadWord x = (map toLower x) `elem` ["darn", "blasted", "stinker"]
+-- there are more, but this is a family program
+
diff --git a/plugins/Dot.hs b/plugins/Dot.hs
new file mode 100644
--- /dev/null
+++ b/plugins/Dot.hs
@@ -0,0 +1,45 @@
+module Dot (plugin) where
+
+-- This plugin allows you to include a graphviz dot diagram
+-- in a page like this:
+--
+-- ~~~ {.dot name="diagram1"}
+-- digraph G {Hello->World}
+-- ~~~
+--
+-- The "dot" executable must be in the path.
+-- The generated png file will be saved in the static img directory.
+-- If no name is specified, a unique name will be generated from a hash
+-- of the file contents.
+
+import Network.Gitit.Interface
+import System.Process
+import System.Exit
+-- from the utf8-string package on HackageDB:
+import Data.ByteString.Lazy.UTF8 (fromString)
+-- from the SHA package on HackageDB:
+import Data.Digest.Pure.SHA
+import System.FilePath
+import Control.Monad.Trans (liftIO)
+
+plugin :: Plugin
+plugin = mkPageTransformM transformBlock
+
+transformBlock :: Block -> PluginM Block
+transformBlock (CodeBlock (_, classes, namevals) contents) | "dot" `elem` classes = do
+  cfg <- askConfig
+  let (name, outfile) =  case lookup "name" namevals of
+                                Just fn   -> ([Str fn], fn ++ ".png")
+                                Nothing   -> ([], uniqueName contents ++ ".png")
+  liftIO $ do
+    (ec, out, err) <- readProcessWithExitCode "dot" ["-Tpng"] contents
+    if ec == ExitSuccess
+       then writeFile (staticDir cfg </> "img" </> outfile) out
+       else error $ "dot returned an error status: " ++ err
+  return $ Para [Image name ("/img" </> outfile, "")]
+transformBlock x = return x
+
+-- | Generate a unique filename given the file's contents.
+uniqueName :: String -> String
+uniqueName = showDigest . sha1 . fromString
+
diff --git a/plugins/ImgTex.hs b/plugins/ImgTex.hs
new file mode 100644
--- /dev/null
+++ b/plugins/ImgTex.hs
@@ -0,0 +1,75 @@
+module ImgTex (plugin) where
+{-
+
+This plugin provides a clear math LaTeX output.
+(* latex and dvipng executable must be in the path.)
+
+like this:
+
+~~~ {.dvipng}
+\nabla \times \bm{V}
+=
+\frac{1}{h_1 h_2 h_3}
+  \begin{vmatrix}
+    h_1 e_1 & h_2 e_2 & h_3 e_3 \\
+    \frac{\partial}{\partial q_{1}} &
+    \frac{\partial}{\partial q_{2}} &
+    \frac{\partial}{\partial q_{3}} \\
+    h_1 V_1 & h_2 V_2 & h_3 V_3
+  \end{vmatrix}
+~~~
+
+License: GPL
+written by Kohei OZAKI <i@smly.org>
+modified by John MacFarlane to use withTempDir
+
+-}
+
+import Network.Gitit.Interface
+import Text.Pandoc.Shared
+import System.Process (system)
+import System.Exit
+import System.Directory
+import Data.Char (ord)
+import Data.ByteString.Lazy.UTF8 (fromString)
+import Data.Digest.Pure.SHA
+import System.FilePath
+import Control.Monad.Trans (liftIO)
+
+plugin :: Plugin
+plugin = mkPageTransformM transformBlock
+
+templateHeader =
+    ( "\\documentclass[12pt]{article}\n"
+      ++ "\\usepackage{amsmath,amssymb,bm}\n"
+      ++ "\\begin{document}\n"
+      ++ "\\thispagestyle{empty}\n"
+      ++ "\\[\n"
+    )
+
+templateFooter =
+    ( "\n"
+      ++ "\\]\n"
+      ++ "\\end{document}\n"
+    )
+
+transformBlock :: Block -> PluginM Block
+transformBlock (CodeBlock (id, classes, namevals) contents)
+    | "dvipng" `elem` classes = do
+  cfg <- askConfig
+  let (name, outfile) =  case lookup "name" namevals of
+                                Just fn   -> ([Str fn], fn ++ ".png")
+                                Nothing   -> ([], uniqueName contents ++ ".png")
+  curr <- liftIO getCurrentDirectory
+  liftIO $ withTempDir "gitit-imgtex" $ \tmpdir -> do
+    setCurrentDirectory tmpdir
+    writeFile (outfile ++ ".tex") (templateHeader ++ contents ++ templateFooter)
+    system $ "latex " ++ outfile ++ ".tex > /dev/null"
+    setCurrentDirectory curr
+    system $ "dvipng -T tight -bd 1000 -freetype0 -Q 5 --gamma 1.3 " ++
+              (tmpdir </> outfile <.> "dvi") ++ " -o " ++ (staticDir cfg </> "img" </> outfile)
+    return $ Para [Image name ("/img" </> outfile, "")]
+transformBlock x = return x
+
+uniqueName :: String -> String
+uniqueName = showDigest . sha1 . fromString
diff --git a/plugins/Interwiki.hs b/plugins/Interwiki.hs
new file mode 100644
--- /dev/null
+++ b/plugins/Interwiki.hs
@@ -0,0 +1,432 @@
+{- | This plugin causes link URLs of the form wikiname!articlename to be
+  treated as interwiki links.  So, for example,
+
+> [The Emperor Palpatine](!Wookieepedia "Emperor Palpatine")
+
+  links to the article on "Emperor Palpatine" in Wookieepedia
+  (<http://starwars.wikia.com/wiki/Emperor_Palpatine>).
+
+  This module also supports a shorter syntax, for when the link text
+  is identical to the article name. Example:
+
+> [Emperor Palpatine](!Wookieepedia)
+
+  will link to the right place, same as the previous example.
+
+  (Written by Gwern Branwen; put in public domain, 2009) -}
+
+module Interwiki (plugin) where
+
+import Network.Gitit.Interface
+
+import qualified Data.Map as M (fromList, lookup, Map)
+import Network.URI (escapeURIString, isAllowedInURI, unEscapeString)
+
+plugin :: Plugin
+plugin = mkPageTransform convertInterwikiLinks
+
+{- | A good interwiki link looks like '!Wookieepedia "Emperor Palpatine"'. So we check for a leading '!'.
+     We strip it off, and now we have the canonical sitename (in this case, "Wookieepedia" and we look it up
+     in our database.
+     The database should return the URL for that site; we only need append the (escaped) article name to that,
+     and we have the full URL! If there isn't one there, then we look back at the link-text for the article
+     name; this is how we support the shortened syntax (see module description).
+     If there isn't a leading '!', we get back a Nothing (the database doesn't know the site), we just return
+     the Link unchanged. -}
+convertInterwikiLinks :: Inline -> Inline
+convertInterwikiLinks (Link ref (interwiki, article)) =
+  case interwiki of
+    ('!':interwiki') ->
+        case M.lookup interwiki' interwikiMap of
+                Just url  -> case article of
+                                  "" -> Link ref (url ++ (inlinesToURL ref), (summary $ unEscapeString $ inlinesToURL ref))
+                                  _  -> Link ref (interwikiurl article url, summary article)
+                Nothing -> Link ref (interwiki, article)
+            where -- 'http://starwars.wikia.com/wiki/Emperor_Palpatine'
+                  interwikiurl a u = escapeURIString isAllowedInURI $ u ++ a
+                  -- 'Wookieepedia: Emperor Palpatine'
+                  summary a = interwiki' ++ ": " ++ a
+    _ -> Link ref (interwiki, article)
+convertInterwikiLinks x = x
+
+{- | Large table of constants; this is a mapping from shortcuts to a URL. The URL can be used by
+   appending to it the article name (suitably URL-escaped, of course).
+   The mapping is derived from <https://secure.wikimedia.org/wikipedia/meta/wiki/Interwiki_map>
+   as of 11:19 PM, 11 February 2009. -}
+interwikiMap :: M.Map String String
+interwikiMap = M.fromList [ ("AbbeNormal", "http://ourpla.net/cgi/pikie?"),
+                 ("Acronym", "http://www.acronymfinder.com/af-query.asp?String=exact&Acronym="),
+                 ("Advisory", "http://advisory.wikimedia.org/wiki/"),
+                 ("Advogato", "http://www.advogato.org/"),
+                 ("Aew", "http://wiki.arabeyes.org/"),
+                 ("Airwarfare", "http://airwarfare.com/mediawiki-1.4.5/index.php?"),
+                 ("AIWiki", "http://www.ifi.unizh.ch/ailab/aiwiki/aiw.cgi?"),
+                 ("AllWiki", "http://allwiki.com/index.php/"),
+                 ("Appropedia", "http://www.appropedia.org/"),
+                 ("AquariumWiki", "http://www.theaquariumwiki.com/"),
+                 ("arXiv", "http://arxiv.org/abs/"),
+                 ("AspieNetWiki", "http://aspie.mela.de/index.php/"),
+                 ("AtmWiki", "http://www.otterstedt.de/wiki/index.php/"),
+                 ("BattlestarWiki", "http://en.battlestarwiki.org/wiki/"),
+                 ("BEMI", "http://bemi.free.fr/vikio/index.php?"),
+                 ("BenefitsWiki", "http://www.benefitslink.com/cgi-bin/wiki.cgi?"),
+                 ("betawiki", "http://translatewiki.net/wiki/"),
+                 ("BibleWiki", "http://bible.tmtm.com/wiki/"),
+                 ("BluWiki", "http://www.bluwiki.org/go/"),
+                 ("Botwiki", "http://botwiki.sno.cc/wiki/"),
+                 ("Boxrec", "http://www.boxrec.com/media/index.php?"),
+                 ("BrickWiki", "http://brickwiki.org/index.php?title="),
+                 ("BridgesWiki", "http://c2.com:8000/"),
+                 ("bugzilla", "https://bugzilla.wikimedia.org/show_bug.cgi?id="),
+                 ("buzztard", "http://buzztard.org/index.php/"),
+                 ("Bytesmiths", "http://www.Bytesmiths.com/wiki/"),
+                 ("C2", "http://c2.com/cgi/wiki?"),
+                 ("C2find", "http://c2.com/cgi/wiki?FindPage&value="),
+                 ("Cache", "http://www.google.com/search?q=cache:"),
+                 ("CanyonWiki", "http://www.canyonwiki.com/wiki/index.php/"),
+                 ("CANWiki", "http://www.can-wiki.info/"),
+                 ("ĈEJ", "http://esperanto.blahus.cz/cxej/vikio/index.php/"),
+                 ("CellWiki", "http://cell.wikia.com/wiki/"),
+                 ("CentralWikia", "http://www.wikia.com/wiki/"),
+                 ("ChEJ", "http://esperanto.blahus.cz/cxej/vikio/index.php/"),
+                 ("ChoralWiki", "http://www.cpdl.org/wiki/index.php/"),
+                 ("Ciscavate", "http://ciscavate.org/index.php/"),
+                 ("Citizendium", "http://en.citizendium.org/wiki/"),
+                 ("CKWiss", "http://ck-wissen.de/ckwiki/index.php?title="),
+                 ("CNDbName", "http://cndb.com/actor.html?name="),
+                 ("CNDbTitle", "http://cndb.com/movie.html?title="),
+                 ("CoLab", "http://colab.info"),
+                 ("Comixpedia", "http://www.comixpedia.org/index.php/"),
+                 ("comcom", "http://comcom.wikimedia.org/wiki/"),
+                 ("Commons", "http://commons.wikimedia.org/wiki/"),
+                 ("CommunityScheme", "http://community.schemewiki.org/?c=s&key="),
+                 ("comune", "http://rete.comuni-italiani.it/wiki/"),
+                 ("Consciousness", "http://teadvus.inspiral.org/index.php/"),
+                 ("CorpKnowPedia", "http://corpknowpedia.org/wiki/index.php/"),
+                 ("CrazyHacks", "http://www.crazy-hacks.org/wiki/index.php?title="),
+                 ("CreaturesWiki", "http://creatures.wikia.com/wiki/"),
+                 ("CxEJ", "http://esperanto.blahus.cz/cxej/vikio/index.php/"),
+                 ("DAwiki", "http://www.dienstag-abend.de/wiki/index.php/"),
+                 ("Dcc", "http://www.dccwiki.com/"),
+                 ("DCDatabase", "http://www.dcdatabaseproject.com/wiki/"),
+                 ("DCMA", "http://www.christian-morgenstern.de/dcma/"),
+                 ("DejaNews", "http://www.deja.com/=dnc/getdoc.xp?AN="),
+                 ("Delicious", "http://del.icio.us/tag/"),
+                 ("Demokraatia", "http://wiki.demokraatia.ee/index.php/"),
+                 ("Devmo", "http://developer.mozilla.org/en/docs/"),
+                 ("Dictionary", "http://www.dict.org/bin/Dict?Database=*&Form=Dict1&Strategy=*&Query="),
+                 ("Dict", "http://www.dict.org/bin/Dict?Database=*&Form=Dict1&Strategy=*&Query="),
+                 ("Disinfopedia", "http://www.sourcewatch.org/wiki.phtml?title="),
+                 ("distributedproofreaders", "http://www.pgdp.net/wiki/"),
+                 ("distributedproofreadersca", "http://www.pgdpcanada.net/wiki/index.php/"),
+                 ("dmoz", "http://www.dmoz.org/"),
+                 ("dmozs", "http://www.dmoz.org/cgi-bin/search?search="),
+                 ("DocBook", "http://wiki.docbook.org/topic/"),
+                 ("DOI", "http://dx.doi.org/"),
+                 ("doom_wiki", "http://doom.wikia.com/wiki/"),
+                 ("download", "http://download.wikimedia.org/"),
+                 ("dbdump", "http://download.wikimedia.org//latest/"),
+                 ("DRAE", "http://buscon.rae.es/draeI/SrvltGUIBusUsual?LEMA="),
+                 ("Dreamhost", "http://wiki.dreamhost.com/index.php/"),
+                 ("DrumCorpsWiki", "http://www.drumcorpswiki.com/index.php/"),
+                 ("DWJWiki", "http://www.suberic.net/cgi-bin/dwj/wiki.cgi?"),
+                 ("EĉeI", "http://www.ikso.net/cgi-bin/wiki.pl?"),
+                 ("EcheI", "http://www.ikso.net/cgi-bin/wiki.pl?"),
+                 ("EcoReality", "http://www.EcoReality.org/wiki/"),
+                 ("EcxeI", "http://www.ikso.net/cgi-bin/wiki.pl?"),
+                 ("EfnetCeeWiki", "http://purl.net/wiki/c/"),
+                 ("EfnetCppWiki", "http://purl.net/wiki/cpp/"),
+                 ("EfnetPythonWiki", "http://purl.net/wiki/python/"),
+                 ("EfnetXmlWiki", "http://purl.net/wiki/xml/"),
+                 ("ELibre", "http://enciclopedia.us.es/index.php/"),
+                 ("EmacsWiki", "http://www.emacswiki.org/cgi-bin/wiki.pl?"),
+                 ("EnergieWiki", "http://www.netzwerk-energieberater.de/wiki/index.php/"),
+                 ("EoKulturCentro", "http://esperanto.toulouse.free.fr/nova/wikini/wakka.php?wiki="),
+                 ("Ethnologue", "http://www.ethnologue.com/show_language.asp?code="),
+                 ("EvoWiki", "http://wiki.cotch.net/index.php/"),
+                 ("Exotica", "http://www.exotica.org.uk/wiki/"),
+                 ("FanimutationWiki", "http://wiki.animutationportal.com/index.php/"),
+                 ("FinalEmpire", "http://final-empire.sourceforge.net/cgi-bin/wiki.pl?"),
+                 ("FinalFantasy", "http://finalfantasy.wikia.com/wiki/"),
+                 ("Finnix", "http://www.finnix.org/"),
+                 ("FlickrUser", "http://www.flickr.com/people/"),
+                 ("FloralWIKI", "http://www.floralwiki.co.uk/wiki/"),
+                 ("FlyerWiki-de", "http://de.flyerwiki.net/index.php/"),
+                 ("Foldoc", "http://www.foldoc.org/"),
+                 ("ForthFreak", "http://wiki.forthfreak.net/index.cgi?"),
+                 ("Foundation", "http://wikimediafoundation.org/wiki/"),
+                 ("FoxWiki", "http://fox.wikis.com/wc.dll?Wiki~"),
+                 ("FreeBio", "http://freebiology.org/wiki/"),
+                 ("FreeBSDman", "http://www.FreeBSD.org/cgi/man.cgi?apropos=1&query="),
+                 ("FreeCultureWiki", "http://wiki.freeculture.org/index.php/"),
+                 ("Freedomdefined", "http://freedomdefined.org/"),
+                 ("FreeFeel", "http://freefeel.org/wiki/"),
+                 ("FreekiWiki", "http://wiki.freegeek.org/index.php/"),
+                 ("Freenode", "http://ganfyd.org/index.php?title="),
+                 ("GaussWiki", "http://gauss.ffii.org/"),
+                 ("Gentoo-Wiki", "http://gentoo-wiki.com/"),
+                 ("GenWiki", "http://wiki.genealogy.net/index.php/"),
+                 ("GlobalVoices", "http://cyber.law.harvard.edu/dyn/globalvoices/wiki/"),
+                 ("GlossarWiki", "http://glossar.hs-augsburg.de/"),
+                 ("GlossaryWiki", "http://glossary.hs-augsburg.de/"),
+                 ("Golem", "http://golem.linux.it/index.php/"),
+                 ("Google", "http://www.google.com/search?q="),
+                 ("GoogleDefine", "http://www.google.com/search?q=define:"),
+                 ("GoogleGroups", "http://groups.google.com/groups?q="),
+                 ("GotAMac", "http://www.got-a-mac.org/"),
+                 ("GreatLakesWiki", "http://greatlakeswiki.org/index.php/"),
+                 ("Guildwiki", "http://gw.gamewikis.org/wiki/"),
+                 ("gutenberg", "http://www.gutenberg.org/etext/"),
+                 ("gutenbergwiki", "http://www.gutenberg.org/wiki/"),
+                 ("H2Wiki", "http://halowiki.net/p/"),
+                 ("HammondWiki", "http://www.dairiki.org/HammondWiki/index.php3?"),
+                 ("heroeswiki", "http://heroeswiki.com/"),
+                 ("HerzKinderWiki", "http://www.herzkinderinfo.de/Mediawiki/index.php/"),
+                 ("HKMule", "http://www.hkmule.com/wiki/"),
+                 ("HolshamTraders", "http://www.holsham-traders.de/wiki/index.php/"),
+                 ("HRWiki", "http://www.hrwiki.org/index.php/"),
+                 ("HRFWiki", "http://fanstuff.hrwiki.org/index.php/"),
+                 ("HumanCell", "http://www.humancell.org/index.php/"),
+                 ("HupWiki", "http://wiki.hup.hu/index.php/"),
+                 ("IMDbName", "http://www.imdb.com/name/nm/"),
+                 ("IMDbTitle", "http://www.imdb.com/title/tt/"),
+                 ("IMDbCompany", "http://www.imdb.com/company/co/"),
+                 ("IMDbCharacter", "http://www.imdb.com/character/ch/"),
+                 ("Incubator", "http://incubator.wikimedia.org/wiki/"),
+                 ("infoAnarchy", "http://www.infoanarchy.org/en/"),
+                 ("Infosecpedia", "http://www.infosecpedia.org/pedia/index.php/"),
+                 ("Infosphere", "http://theinfosphere.org/"),
+                 ("IRC", "http://www.sil.org/iso639-3/documentation.asp?id="),
+                 ("Iuridictum", "http://iuridictum.pecina.cz/w/"),
+                 ("JamesHoward", "http://jameshoward.us/"),
+                 ("JavaNet", "http://wiki.java.net/bin/view/Main/"),
+                 ("Javapedia", "http://wiki.java.net/bin/view/Javapedia/"),
+                 ("JEFO", "http://esperanto-jeunes.org/wiki/"),
+                 ("JiniWiki", "http://www.cdegroot.com/cgi-bin/jini?"),
+                 ("JspWiki", "http://www.ecyrd.com/JSPWiki/Wiki.jsp?page="),
+                 ("JSTOR", "http://www.jstor.org/journals/"),
+                 ("Kamelo", "http://kamelopedia.mormo.org/index.php/"),
+                 ("Karlsruhe", "http://ka.stadtwiki.net/"),
+                 ("KerimWiki", "http://wiki.oxus.net/"),
+                 ("KinoWiki", "http://kino.skripov.com/index.php/"),
+                 ("KmWiki", "http://kmwiki.wikispaces.com/"),
+                 ("KontuWiki", "http://kontu.merri.net/wiki/"),
+                 ("KoslarWiki", "http://wiki.koslar.de/index.php/"),
+                 ("Kpopwiki", "http://www.kpopwiki.com/"),
+                 ("LinguistList", "http://linguistlist.org/forms/langs/LLDescription.cfm?code="),
+                 ("LinuxWiki", "http://www.linuxwiki.de/"),
+                 ("LinuxWikiDe", "http://www.linuxwiki.de/"),
+                 ("LISWiki", "http://liswiki.org/wiki/"),
+                 ("LiteratePrograms", "http://en.literateprograms.org/"),
+                 ("Livepedia", "http://www.livepedia.gr/index.php?title="),
+                 ("Lojban", "http://www.lojban.org/tiki/tiki-index.php?page="),
+                 ("Lostpedia", "http://en.lostpedia.com/wiki/"),
+                 ("LQWiki", "http://wiki.linuxquestions.org/wiki/"),
+                 ("LugKR", "http://lug-kr.sourceforge.net/cgi-bin/lugwiki.pl?"),
+                 ("Luxo", "http://toolserver.org/~luxo/contributions/contributions.php?user="),
+                 ("lyricwiki", "http://www.lyricwiki.org/"),
+                 ("Mail", "https://lists.wikimedia.org/mailman/listinfo/"),
+                 ("mailarchive", "http://lists.wikimedia.org/pipermail/"),
+                 ("Mariowiki", "http://www.mariowiki.com/"),
+                 ("MarvelDatabase", "http://www.marveldatabase.com/wiki/index.php/"),
+                 ("MeatBall", "http://www.usemod.com/cgi-bin/mb.pl?"),
+                 ("MediaZilla", "https://bugzilla.wikimedia.org/"),
+                 ("MemoryAlpha", "http://memory-alpha.org/en/wiki/"),
+                 ("MetaWiki", "http://sunir.org/apps/meta.pl?"),
+                 ("MetaWikiPedia", "http://meta.wikimedia.org/wiki/"),
+                 ("Mineralienatlas", "http://www.mineralienatlas.de/lexikon/index.php/"),
+                 ("MoinMoin", "http://moinmo.in/"),
+                 ("Monstropedia", "http://www.monstropedia.org/?title="),
+                 ("MosaPedia", "http://mosapedia.de/wiki/index.php/"),
+                 ("MozCom", "http://mozilla.wikia.com/wiki/"),
+                 ("MozillaWiki", "http://wiki.mozilla.org/"),
+                 ("MozillaZineKB", "http://kb.mozillazine.org/"),
+                 ("MusicBrainz", "http://wiki.musicbrainz.org/"),
+                 ("MW", "http://www.mediawiki.org/wiki/"),
+                 ("MWOD", "http://www.merriam-webster.com/cgi-bin/dictionary?book=Dictionary&va="),
+                 ("MWOT", "http://www.merriam-webster.com/cgi-bin/thesaurus?book=Thesaurus&va="),
+                 ("NetVillage", "http://www.netbros.com/?"),
+                 ("NKcells", "http://www.nkcells.info/wiki/index.php/"),
+                 ("NoSmoke", "http://no-smok.net/nsmk/"),
+                 ("Nost", "http://nostalgia.wikipedia.org/wiki/"),
+                 ("OEIS", "http://www.research.att.com/~njas/sequences/"),
+                 ("OldWikisource", "http://wikisource.org/wiki/"),
+                 ("OLPC", "http://wiki.laptop.org/go/"),
+                 ("OneLook", "http://www.onelook.com/?ls=b&w="),
+                 ("OpenFacts", "http://openfacts.berlios.de/index.phtml?title="),
+                 ("Openstreetmap", "http://wiki.openstreetmap.org/index.php/"),
+                 ("OpenWetWare", "http://openwetware.org/wiki/"),
+                 ("OpenWiki", "http://openwiki.com/?"),
+                 ("Opera7Wiki", "http://operawiki.info/"),
+                 ("OrganicDesign", "http://www.organicdesign.co.nz/"),
+                 ("OrgPatterns", "http://www.bell-labs.com/cgi-user/OrgPatterns/OrgPatterns?"),
+                 ("OrthodoxWiki", "http://orthodoxwiki.org/"),
+                 ("OSI", "http://wiki.tigma.ee/index.php/"),
+                 ("OTRS", "https://secure.wikimedia.org/otrs/index.pl?Action=AgentTicketZoom&TicketID="),
+                 ("OTRSwiki", "http://otrs-wiki.wikimedia.org/wiki/"),
+                 ("OurMedia", "http://www.socialtext.net/ourmedia/index.cgi?"),
+                 ("PaganWiki", "http://www.paganwiki.org/wiki/index.php?title="),
+                 ("Panawiki", "http://wiki.alairelibre.net/wiki/"),
+                 ("PangalacticOrg", "http://www.pangalactic.org/Wiki/"),
+                 ("PatWIKI", "http://gauss.ffii.org/"),
+                 ("PerlConfWiki", "http://perl.conf.hu/index.php/"),
+                 ("PerlNet", "http://perl.net.au/wiki/"),
+                 ("PersonalTelco", "http://www.personaltelco.net/index.cgi/"),
+                 ("PHWiki", "http://wiki.pocketheaven.com/"),
+                 ("PhpWiki", "http://phpwiki.sourceforge.net/phpwiki/index.php?"),
+                 ("PlanetMath", "http://planetmath.org/?op=getobj&from=objects&id="),
+                 ("PMEG", "http://www.bertilow.com/pmeg/.php"),
+                 ("PMWiki", "http://old.porplemontage.com/wiki/index.php/"),
+                 ("PurlNet", "http://purl.oclc.org/NET/"),
+                 ("PythonInfo", "http://www.python.org/cgi-bin/moinmoin/"),
+                 ("PythonWiki", "http://www.pythonwiki.de/"),
+                 ("PyWiki", "http://c2.com/cgi/wiki?"),
+                 ("psycle", "http://psycle.sourceforge.net/wiki/"),
+                 ("qcwiki", "http://wiki.quantumchemistry.net/index.php/"),
+                 ("Quality", "http://quality.wikimedia.org/wiki/"),
+                 ("Qwiki", "http://qwiki.caltech.edu/wiki/"),
+                 ("r3000", "http://prinsig.se/weekee/"),
+                 ("RakWiki", "http://rakwiki.no-ip.info/"),
+                 ("Raec", "http://www.raec.clacso.edu.ar:8080/raec/Members/raecpedia/"),
+                 ("rev", "http://www.mediawiki.org/wiki/Special:Code/MediaWiki/"),
+                 ("ReVo", "http://purl.org/NET/voko/revo/art/.html"),
+                 ("RFC", "http://tools.ietf.org/html/rfc"),
+                 ("RheinNeckar", "http://wiki.rhein-neckar.de/index.php/"),
+                 ("RoboWiki", "http://robowiki.net/?"),
+                 ("ReutersWiki", "http://glossary.reuters.com/index.php/"),
+                 ("RoWiki", "http://wiki.rennkuckuck.de/index.php/"),
+                 ("rtfm", "http://s23.org/wiki/"),
+                 ("Scholar", "http://scholar.google.com/scholar?q="),
+                 ("SchoolsWP", "http://schools-wikipedia.org/wiki/"),
+                 ("Scores", "http://www.imslp.org/wiki/"),
+                 ("Scoutwiki", "http://en.scoutwiki.org/"),
+                 ("Scramble", "http://www.scramble.nl/wiki/index.php?title="),
+                 ("SeaPig", "http://www.seapig.org/"),
+                 ("SeattleWiki", "http://seattlewiki.org/wiki/"),
+                 ("SeattleWireless", "http://seattlewireless.net/?"),
+                 ("SLWiki", "http://wiki.secondlife.com/wiki/"),
+                 ("SenseisLibrary", "http://senseis.xmp.net/?"),
+                 ("silcode", "http://www.sil.org/iso639-3/documentation.asp?id="),
+                 ("Shakti", "http://cgi.algonet.se/htbin/cgiwrap/pgd/ShaktiWiki/"),
+                 ("Slashdot", "http://slashdot.org/article.pl?sid="),
+                 ("SMikipedia", "http://www.smiki.de/"),
+                 ("SourceForge", "http://sourceforge.net/"),
+                 ("spcom", "http://spcom.wikimedia.org/wiki/"),
+                 ("Species", "http://species.wikimedia.org/wiki/"),
+                 ("Squeak", "http://wiki.squeak.org/squeak/"),
+                 ("stable", "http://stable.toolserver.org/"),
+                 ("StrategyWiki", "http://strategywiki.org/wiki/"),
+                 ("Sulutil", "http://toolserver.org/~vvv/sulutil.php?user="),
+                 ("Susning", "http://www.susning.nu/"),
+                 ("Swtrain", "http://train.spottingworld.com/"),
+                 ("svn", "http://svn.wikimedia.org/viewvc/mediawiki/?view=log"),
+                 ("SVGWiki", "http://www.protocol7.com/svg-wiki/default.asp?"),
+                 ("SwinBrain", "http://mercury.it.swin.edu.au/swinbrain/index.php/"),
+                 ("SwingWiki", "http://www.swingwiki.org/"),
+                 ("TabWiki", "http://www.tabwiki.com/index.php/"),
+                 ("Takipedia", "http://www.takipedia.org/wiki/"),
+                 ("Tavi", "http://tavi.sourceforge.net/"),
+                 ("TclersWiki", "http://wiki.tcl.tk/"),
+                 ("Technorati", "http://www.technorati.com/search/"),
+                 ("TEJO", "http://www.tejo.org/vikio/"),
+                 ("TESOLTaiwan", "http://www.tesol-taiwan.org/wiki/index.php/"),
+                 ("Testwiki", "http://test.wikipedia.org/wiki/"),
+                 ("Thelemapedia", "http://www.thelemapedia.org/index.php/"),
+                 ("Theopedia", "http://www.theopedia.com/"),
+                 ("ThePPN", "http://wiki.theppn.org/"),
+                 ("ThinkWiki", "http://www.thinkwiki.org/wiki/"),
+                 ("TibiaWiki", "http://tibia.erig.net/"),
+                 ("Ticket", "https://secure.wikimedia.org/otrs/index.pl?Action=AgentTicketZoom&TicketNumber="),
+                 ("TMBW", "http://tmbw.net/wiki/"),
+                 ("TmNet", "http://www.technomanifestos.net/?"),
+                 ("TMwiki", "http://www.EasyTopicMaps.com/?page="),
+                 ("TokyoNights", "http://wiki.tokyo-nights.com/wiki/"),
+                 ("Tools", "http://toolserver.org/"),
+                 ("tswiki", "http://wiki.toolserver.org/view/"),
+                 ("translatewiki", "http://translatewiki.net/wiki/"),
+                 ("Trash!Italia", "http://trashware.linux.it/wiki/"),
+                 ("Turismo", "http://www.tejo.org/turismo/"),
+                 ("TVIV", "http://tviv.org/wiki/"),
+                 ("TVtropes", "http://www.tvtropes.org/pmwiki/pmwiki.php/Main/"),
+                 ("TWiki", "http://twiki.org/cgi-bin/view/"),
+                 ("TwistedWiki", "http://purl.net/wiki/twisted/"),
+                 ("TyvaWiki", "http://www.tyvawiki.org/wiki/"),
+                 ("Uncyclopedia", "http://uncyclopedia.org/wiki/"),
+                 ("Unreal", "http://wiki.beyondunreal.com/wiki/"),
+                 ("Urbandict", "http://www.urbandictionary.com/define.php?term="),
+                 ("USEJ", "http://www.tejo.org/usej/"),
+                 ("UseMod", "http://www.usemod.com/cgi-bin/wiki.pl?"),
+                 ("ValueWiki", "http://www.valuewiki.com/w/"),
+                 ("Veropedia", "http://en.veropedia.com/a/"),
+                 ("Vinismo", "http://vinismo.com/en/"),
+                 ("VLOS", "http://www.thuvienkhoahoc.com/tusach/"),
+                 ("VKoL", "http://kol.coldfront.net/thekolwiki/index.php/"),
+                 ("VoIPinfo", "http://www.voip-info.org/wiki/view/"),
+                 ("WarpedView", "http://www.warpedview.com/mediawiki/index.php/"),
+                 ("WebDevWikiNL", "http://www.promo-it.nl/WebDevWiki/index.php?page="),
+                 ("Webisodes", "http://www.webisodes.org/"),
+                 ("WebSeitzWiki", "http://webseitz.fluxent.com/wiki/"),
+                 ("wg", "http://wg.en.wikipedia.org/wiki/"),
+                 ("Wiki", "http://c2.com/cgi/wiki?"),
+                 ("Wikia", "http://www.wikia.com/wiki/c:"),
+                 ("WikiaSite", "http://www.wikia.com/wiki/c:"),
+                 ("Wikianso", "http://www.ansorena.de/mediawiki/wiki/"),
+                 ("Wikible", "http://wikible.org/en/"),
+                 ("Wikibooks", "http://en.wikibooks.org/wiki/"),
+                 ("Wikichat", "http://www.wikichat.org/"),
+                 ("WikiChristian", "http://www.wikichristian.org/index.php?title="),
+                 ("Wikicities", "http://www.wikia.com/wiki/"),
+                 ("Wikicity", "http://www.wikia.com/wiki/c:"),
+                 ("WikiF1", "http://www.wikif1.org/"),
+                 ("WikiFur", "http://en.wikifur.com/wiki/"),
+                 ("wikiHow", "http://www.wikihow.com/"),
+                 ("WikiIndex", "http://wikiindex.com/"),
+                 ("WikiLemon", "http://wiki.illemonati.com/"),
+                 ("Wikilivres", "http://wikilivres.info/wiki/"),
+                 ("WikiMac-de", "http://apfelwiki.de/wiki/Main/"),
+                 ("WikiMac-fr", "http://www.wikimac.org/index.php/"),
+                 ("Wikimedia", "http://wikimediafoundation.org/wiki/"),
+                 ("Wikinews", "http://en.wikinews.org/wiki/"),
+                 ("Wikinfo", "http://www.wikinfo.org/index.php/"),
+                 ("Wikinurse", "http://wikinurse.org/media/index.php?title="),
+                 ("Wikinvest", "http://www.wikinvest.com/"),
+                 ("Wikipaltz", "http://www.wikipaltz.com/wiki/"),
+                 ("Wikipedia", "http://en.wikipedia.org/wiki/"),
+                 ("WikipediaWikipedia", "http://en.wikipedia.org/wiki/Wikipedia:"),
+                 ("Wikiquote", "http://en.wikiquote.org/wiki/"),
+                 ("Wikireason", "http://wikireason.net/wiki/"),
+                 ("Wikischool", "http://www.wikischool.de/wiki/"),
+                 ("wikisophia", "http://wikisophia.org/index.php?title="),
+                 ("Wikisource", "http://en.wikisource.org/wiki/"),
+                 ("Wikispecies", "http://species.wikimedia.org/wiki/"),
+                 ("Wikispot", "http://wikispot.org/?action=gotowikipage&v="),
+                 ("WikiTI", "http://wikiti.denglend.net/index.php?title="),
+                 ("WikiTravel", "http://wikitravel.org/en/"),
+                 ("WikiTree", "http://wikitree.org/index.php?title="),
+                 ("Wikiversity", "http://en.wikiversity.org/wiki/"),
+                 ("betawikiversity", "http://beta.wikiversity.org/wiki/"),
+                 ("WikiWikiWeb", "http://c2.com/cgi/wiki?"),
+                 ("Wiktionary", "http://en.wiktionary.org/wiki/"),
+                 ("Wipipedia", "http://www.londonfetishscene.com/wipi/index.php/"),
+                 ("WLUG", "http://www.wlug.org.nz/"),
+                 ("wmcz", "http://meta.wikimedia.org/wiki/Wikimedia_Czech_Republic/"),
+                 ("Wm2005", "http://wikimania2005.wikimedia.org/wiki/"),
+                 ("Wm2006", "http://wikimania2006.wikimedia.org/wiki/"),
+                 ("Wm2007", "http://wikimania2007.wikimedia.org/wiki/"),
+                 ("Wm2008", "http://wikimania2008.wikimedia.org/wiki/"),
+                 ("Wm2009", "http://wikimania2009.wikimedia.org/wiki/"),
+                 ("Wmania", "http://wikimania.wikimedia.org/wiki/"),
+                 ("WMF", "http://wikimediafoundation.org/wiki/"),
+                 ("wmse", "http://se.wikimedia.org/wiki/"),
+                 ("wmrs", "http://rs.wikimedia.org/wiki/"),
+                 ("Wookieepedia", "http://starwars.wikia.com/wiki/"),
+                 ("World66", "http://www.world66.com/"),
+                 ("WoWWiki", "http://www.wowwiki.com/"),
+                 ("Wqy", "http://wqy.sourceforge.net/cgi-bin/index.cgi?"),
+                 ("WurmPedia", "http://www.wurmonline.com/wiki/index.php/"),
+                 ("WZNAN", "http://www.wikiznanie.ru/wiki/article/"),
+                 ("Xboxic", "http://wiki.xboxic.com/"),
+                 ("ZRHwiki", "http://www.zrhwiki.ch/wiki/"),
+                 ("ZUM", "http://wiki.zum.de/"),
+                 ("ZWiki", "http://www.zwiki.org/"),
+                 ("ZZZ", "http://wiki.zzz.ee/index.php/") ]
diff --git a/plugins/ShowUser.hs b/plugins/ShowUser.hs
new file mode 100644
--- /dev/null
+++ b/plugins/ShowUser.hs
@@ -0,0 +1,20 @@
+module ShowUser (plugin) where
+
+-- This plugin replaces $USER$ with the name of the currently logged in
+-- user, or the empty string if no one is logged in.
+
+import Network.Gitit.Interface
+import Data.Maybe (fromMaybe)
+
+plugin :: Plugin
+plugin = mkPageTransformM showuser
+
+showuser :: Inline -> PluginM Inline
+showuser (Math InlineMath x) | x == "USER"  = do
+  doNotCache  -- tell gitit not to cache this page, as it has dynamic content
+  mbUser <- askUser
+  case mbUser of
+       Nothing -> return $ Str ""
+       Just u  -> return $ Str $ uUsername u
+showuser x = return x
+
diff --git a/plugins/Signature.hs b/plugins/Signature.hs
new file mode 100644
--- /dev/null
+++ b/plugins/Signature.hs
@@ -0,0 +1,25 @@
+module Signature (plugin) where
+
+-- This plugin replaces $SIG$ with the username and timestamp
+-- of the last edit, prior to saving the page in the repository.
+
+import Network.Gitit.Interface
+import Data.Maybe (fromMaybe)
+import Data.DateTime
+import Control.Monad
+
+plugin :: Plugin
+plugin = PreCommitTransform replacedate
+
+replacedate :: String -> PluginM String
+replacedate [] = return ""
+replacedate ('$':'S':'I':'G':'$':xs) = do
+  datetime <- liftIO $ getCurrentTime
+  mbuser <- askUser
+  let username = case mbuser of
+                   Nothing  -> "???"
+                   Just u   -> uUsername u
+  let sig = "-- " ++ username ++ " (" ++ formatDateTime "%c" datetime ++ ")"
+  liftM (sig ++ ) $ replacedate xs
+replacedate (x:xs) = liftM (x : ) $ replacedate xs
+
diff --git a/plugins/WebArchiver.hs b/plugins/WebArchiver.hs
new file mode 100644
--- /dev/null
+++ b/plugins/WebArchiver.hs
@@ -0,0 +1,43 @@
+-- | Scans page of Markdown looking for http links. When it finds them, it submits them
+-- to webcitation.org / https://secure.wikimedia.org/wikipedia/en/wiki/WebCite
+--
+-- Limitations:
+-- * Only parses Markdown, not ReST or any other format; this is because 'readMarkdown'
+-- is hardwired into it.
+--
+-- By: Gwern Branwen; placed in the public domain
+
+module WebArchiver (plugin) where
+
+import Network.Gitit.Interface (askUser, liftIO, processWithM, uEmail, Plugin(PreCommitTransform), Inline(Link))
+import Control.Monad (when)
+import Network.URI (isURI)
+import Control.Concurrent (forkIO)
+import Network.HTTP (getRequest, simpleHTTP)
+import Text.Pandoc (defaultParserState, readMarkdown)
+import Control.Monad.Trans (MonadIO)
+
+plugin :: Plugin
+plugin = PreCommitTransform archivePage
+
+-- archivePage :: (MonadIO m) => String -> ReaderT (Config, Maybe User) (StateT IO) String
+archivePage x = do mbUser <- askUser
+                   let email = case mbUser of
+                        Nothing -> "nobody@mailinator.com"
+                        Just u  -> uEmail u
+                   let p = readMarkdown defaultParserState x
+                   -- force evaluation and archiving side-effects
+                   _p' <- liftIO $ processWithM (archiveLinks email) p
+                   return x -- note: this is read-only - don't actually change page!
+
+archiveLinks :: String -> Inline -> IO Inline
+archiveLinks e x@(Link _ (uln, _)) = checkArchive e uln >> return x
+archiveLinks _ x = return x
+
+-- | Error check the URL.
+checkArchive :: (MonadIO m) => String -> String -> m ()
+checkArchive e u = when (isURI u) (liftIO $ archiveURL e u)
+
+archiveURL :: String -> String -> IO ()
+archiveURL eml url = forkIO (openURL ("http://www.webcitation.org/archive?url=" ++ url ++ "&email=" ++ eml) >> return ()) >> return ()
+   where openURL = simpleHTTP . getRequest
