changeset 2411:919edcf02dc0 default tip

Basic flask compatibility
author Radomir Dopieralski <sheep-devel@sheep.art.pl>
date Sun, 19 May 2013 13:16:35 +0200
parents e1f2066a923c
children
files hatta/__init__.py hatta/page.py hatta/request.py hatta/response.py hatta/views.py hatta/wiki.py setup.py tests/test_hatta.py
diffstat 8 files changed, 150 insertions(+), 545 deletions(-) [+]
line wrap: on
line diff
--- a/hatta/__init__.py	Sat May 18 16:10:36 2013 +0200
+++ b/hatta/__init__.py	Sun May 19 13:16:35 2013 +0200
@@ -24,7 +24,6 @@
 from hatta.page import WikiPage, WikiPageText, WikiPageWiki
 from hatta.parser import WikiParser, WikiWikiParser
 from hatta.storage import WikiStorage, WikiSubdirectoryStorage
-from hatta.request import WikiRequest
 from hatta.response import WikiResponse
 from hatta.wiki import Wiki
 from hatta.hg_integration import cmdtable
@@ -37,7 +36,7 @@
     'WikiPage', 'WikiPageText', 'WikiPageWiki',
     'WikiParser', 'WikiWikiParser',
     'WikiStorage', 'WikiSubdirectoryStorage',
-    'Wiki', 'WikiResponse', 'WikiRequest',
+    'Wiki', 'WikiResponse',
     '__version__', 'project_name', 'project_url', 'project_description',
     'cmdtable',
 ]
--- a/hatta/page.py	Sat May 18 16:10:36 2013 +0200
+++ b/hatta/page.py	Sun May 19 13:16:35 2013 +0200
@@ -8,6 +8,7 @@
 
 import werkzeug
 import werkzeug.contrib.atom
+import flask
 
 pygments = None
 try:
@@ -50,6 +51,25 @@
         raise hatta.error.ForbiddenErr(_(u"This page is locked."))
 
 
+def get_author():
+    """Try to guess the author name. Use IP address as last resort."""
+
+    try:
+        cookie = werkzeug.url_unquote(
+            flask.request.cookies.get("author", "")
+        )
+    except UnicodeError:
+        cookie = None
+    try:
+        auth = werkzeug.url_unquote(
+            flask.request.environ.get('REMOTE_USER', "")
+        )
+    except UnicodeError:
+        auth = None
+    author = (flask.request.form.get("author") or cookie or auth or
+              flask.request.remote_addr)
+    return author
+
 
 def get_page(request, title):
     """Creates a page object based on page's mime type"""
@@ -128,9 +148,6 @@
         self.title = title
         self.mime = mime
         # for now we just use the globals from wiki object
-        if request:
-            self.get_url = request.get_url
-            self.get_download_url = request.get_download_url
         self.wiki = wiki
         self.storage = self.wiki.storage
         self.index = self.wiki.index
@@ -141,6 +158,21 @@
         else:
             self.aliases = {}
 
+    def get_url(self, title=None, view=None, method='GET', external=False, **kw):
+        if view is None:
+            view = 'view'
+        if title is not None:
+            kw['title'] = title.strip()
+        return flask.url_for(
+            'hatta.%s' % view,
+            _method=method,
+            _external=external,
+            **kw
+        )
+
+    def get_download_url(self, title):
+        return self.get_url(title, 'download')
+
     def link_alias(self, addr):
         """Find a target address for an alias."""
 
@@ -252,7 +284,6 @@
             yield self.wiki_link(link, label, class_=class_)
 
     def template(self, template_name, **kwargs):
-        template = self.wiki.template_env.get_template(template_name)
         edit_url = None
         if self.title:
             try:
@@ -272,9 +303,7 @@
             'edit_url': edit_url,
         }
         context.update(kwargs)
-        stream = template.stream(**context)
-        stream.enable_buffering(10)
-        return stream
+        return flask.render_template(template_name, **context)
 
     def dependencies(self):
         """Refresh the page when any of those pages was changed."""
@@ -299,7 +328,7 @@
 
     def render_editor(self, preview=None, captcha_error=None):
         _ = self.wiki.gettext
-        author = self.request.get_author()
+        author = get_author()
         if self.title in self.storage:
             comment = _(u'changed')
             (rev, old_date, old_author,
@@ -358,7 +387,7 @@
         """Generate the HTML for the editor."""
 
         _ = self.wiki.gettext
-        author = self.request.get_author()
+        author = get_author()
         lines = []
         try:
             lines = self.storage.page_text(self.title).splitlines(True)
--- a/hatta/request.py	Sat May 18 16:10:36 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-# -*- coding: utf-8 -*-
-
-import werkzeug
-
-import hatta.views
-
-
-class WikiRequest(werkzeug.BaseRequest, werkzeug.ETagRequestMixin):
-    """
-    A Werkzeug's request with additional functions for handling file
-    uploads and wiki-specific link generation.
-    """
-
-    charset = 'utf-8'
-    encoding_errors = 'ignore'
-
-    def __init__(self, wiki, adapter, environ, **kw):
-        werkzeug.BaseRequest.__init__(self, environ, shallow=False, **kw)
-        self.wiki = wiki
-        self.adapter = adapter
-
-    def get_url(self, title=None, view=None, method='GET',
-                external=False, **kw):
-        if view is None:
-            view = 'view'
-        if title is not None:
-            kw['title'] = title.strip()
-        return self.adapter.build(view, kw, method=method,
-                                  force_external=external)
-
-    def get_download_url(self, title):
-        return self.get_url(title, 'download')
-
-    def get_author(self):
-        """Try to guess the author name. Use IP address as last resort."""
-
-        try:
-            cookie = werkzeug.url_unquote(self.cookies.get("author", ""))
-        except UnicodeError:
-            cookie = None
-        try:
-            auth = werkzeug.url_unquote(self.environ.get('REMOTE_USER', ""))
-        except UnicodeError:
-            auth = None
-        author = (self.form.get("author") or cookie or auth or
-                  self.remote_addr)
-        return author
-
--- a/hatta/response.py	Sat May 18 16:10:36 2013 +0200
+++ b/hatta/response.py	Sun May 19 13:16:35 2013 +0200
@@ -7,7 +7,7 @@
              rev=None, size=None):
     """Create a hatta.request.WikiResponse for a page."""
 
-    response = WikiResponse(content, mimetype=mime)
+    response = WikiResponse(list(content), mimetype=mime)
     if rev is None:
         rev, date, author, comment = request.wiki.storage.page_meta(title)
         response.set_etag(u'%s/%s/%d-%s' % (etag,
--- a/hatta/views.py	Sat May 18 16:10:36 2013 +0200
+++ b/hatta/views.py	Sun May 19 13:16:35 2013 +0200
@@ -7,6 +7,8 @@
 import tempfile
 import pkgutil
 
+import flask
+from flask import request
 import werkzeug
 captcha = None
 try:
@@ -27,44 +29,36 @@
 import mercurial
 
 
-class URL(object):
-    """A decorator for marking methods as endpoints for URLs."""
-
-    urls = []
-
-    def __init__(self, url, methods=None):
-        """Create a decorator with specified parameters."""
-
-        self.url = url
-        self.methods = methods or ['GET', 'HEAD']
-
-    def __call__(self, func):
-        """The actual decorator only records the data."""
+class WikiTitleConverter(werkzeug.routing.PathConverter):
+    """Behaves like the path converter, but doesn't match the "+ pages"."""
 
-        self.urls.append((func.__name__, func, self.url, self.methods))
-        return func
-
-    @classmethod
-    def get_rules(cls):
-        """Returns the routing rules."""
+    def to_url(self, value):
+        return werkzeug.url_quote(value.strip(), self.map.charset, safe="/")
 
-        return [
-            werkzeug.routing.Rule(url, endpoint=name, methods=methods)
-            for name, func, url, methods in cls.urls
-        ]
-
-    @classmethod
-    def get_views(cls):
-        """Returns a dict of views."""
-
-        return {name: func for name, func, url, methods in cls.urls}
+    regex = '([^+%]|%[^2]|%2[^Bb]).*'
 
 
-def _serve_default(request, title, content=None, mime=None):
+class WikiAllConverter(werkzeug.routing.BaseConverter):
+    """Matches everything."""
+
+    regex = '.*'
+
+
+views = flask.Blueprint('hatta', __name__)
+
+@views.record
+def add_url_converters(setup_state):
+    setup_state.app.url_map.converters.update(
+        title=WikiTitleConverter,
+        all=WikiAllConverter,
+    )
+
+
+def _serve_default(title, content=None, mime=None):
     """Some pages have their default content."""
 
     if title in request.wiki.storage:
-        return request.wiki.download(request, title)
+        return request.wiki.download(title)
     if content is None:
         content = pkgutil.get_data('hatta', os.path.join('static', title))
     mime = mime or 'application/octet-stream'
@@ -77,10 +71,9 @@
     return response
 
 
-
-@URL('/<title:title>')
-@URL('/')
-def view(request, title=None):
+@views.route('/<title:title>')
+@views.route('/')
+def view(title=None):
     _ = request.wiki.gettext
     if title is None:
         title = request.wiki.front_page
@@ -98,8 +91,8 @@
     etag = '/(%s)' % u','.join(dependencies)
     return hatta.response.response(request, title, html, etag=etag)
 
-@URL('/+history/<title:title>/<int:rev>')
-def revision(request, title, rev):
+@views.route('/+history/<title:title>/<int:rev>')
+def revision(title, rev):
     _ = request.wiki.gettext
     text = request.wiki.storage.revision_text(title, rev)
     link = werkzeug.html.a(werkzeug.html(title),
@@ -118,9 +111,9 @@
     response = hatta.response.response(request, title, html, rev=rev, etag='/old')
     return response
 
-@URL('/+version/')
-@URL('/+version/<title:title>')
-def version(request, title=None):
+@views.route('/+version/')
+@views.route('/+version/<title:title>')
+def version(title=None):
     if title is None:
         version = request.wiki.storage.repo_revision()
     else:
@@ -130,8 +123,8 @@
             version = 0
     return hatta.response.WikiResponse('%d' % version, mimetype="text/plain")
 
-@URL('/+edit/<title:title>', methods=['POST'])
-def save(request, title):
+@views.route('/+edit/<title:title>', methods=['POST'])
+def save(title):
     _ = request.wiki.gettext
     hatta.page.check_lock(request.wiki, title)
     url = request.get_url(title)
@@ -145,7 +138,7 @@
         else:
             lines = [werkzeug.html.p(werkzeug.html(
                 _(u'No preview for binaries.')))]
-        return request.wiki.edit(request, title, preview=lines)
+        return request.wiki.edit(title, preview=lines)
     elif request.form.get('save'):
         if captcha and request.wiki.recaptcha_private_key:
             response = captcha.submit(
@@ -154,10 +147,10 @@
                 request.wiki.recaptcha_private_key, request.remote_addr)
             if not response.is_valid:
                 text = request.form.get("text", '')
-                return request.wiki.edit(request, title, preview=text.split('\n'),
+                return request.wiki.edit(title, preview=text.split('\n'),
                                  captcha_error=response.error_code)
         comment = request.form.get("comment", "")
-        author = request.get_author()
+        author = hatta.page.get_author()
         text = request.form.get("text")
         try:
             parent = int(request.form.get("parent"))
@@ -193,12 +186,12 @@
         request.wiki.index.update_page(page, title, text=text)
     response = werkzeug.routing.redirect(url, code=303)
     response.set_cookie('author',
-                        werkzeug.url_quote(request.get_author()),
+                        werkzeug.url_quote(hatta.page.get_author()),
                         max_age=604800)
     return response
 
-@URL('/+edit/<title:title>', methods=['GET'])
-def edit(request, title, preview=None, captcha_error=None):
+@views.route('/+edit/<title:title>', methods=['GET'])
+def edit(title, preview=None, captcha_error=None):
     hatta.page.check_lock(request.wiki, title)
     exists = title in request.wiki.storage
     if exists:
@@ -216,13 +209,13 @@
     response.headers.add('Cache-Control', 'no-cache')
     return response
 
-@URL('/+feed/atom')
-@URL('/+feed/rss')
-def atom(request):
+@views.route('/+feed/atom')
+@views.route('/+feed/rss')
+def atom():
     _ = request.wiki.gettext
     feed = werkzeug.contrib.atom.AtomFeed(request.wiki.site_name,
         feed_url=request.url,
-        url=request.adapter.build('view', force_external=True),
+        url=flask.url_for('hatta.view', _external=True),
         subtitle=_(u'Track the most recent changes to the wiki '
                    u'in this feed.'))
     history = itertools.islice(request.wiki.storage.history(), None, 10, None)
@@ -232,16 +225,16 @@
             continue
         unique_titles.add(title)
         if rev > 0:
-            url = request.adapter.build('diff', {
+            url = flask.url_for('hatta.diff', _external=True, **{
                 'title': title,
                 'from_rev': rev - 1,
                 'to_rev': rev,
-            }, force_external=True)
+            })
         else:
-            url = request.adapter.build('revision', {
+            url = flask.url_for('hatta.revision', _external=True, **{
                 'title': title,
                 'rev': rev,
-            }, force_external=True)
+            })
         feed.add(title, comment, content_type="text", author=author,
                  url=url, updated=date)
     rev = request.wiki.storage.repo_revision()
@@ -250,8 +243,8 @@
     response.make_conditional(request)
     return response
 
-@URL('/+download/<title:title>')
-def download(request, title):
+@views.route('/+download/<title:title>')
+def download(title):
     """Serve the raw content of a page directly from disk."""
 
     mime = hatta.page.page_mime(title)
@@ -263,8 +256,8 @@
     response.direct_passthrough = True
     return response
 
-@URL('/+render/<title:title>')
-def render(request, title):
+@views.route('/+render/<title:title>')
+def render(title):
     """Serve a thumbnail or otherwise rendered content."""
 
     def file_time_and_size(file_path):
@@ -302,7 +295,7 @@
         cache_filename, cache_mime = page.render_mime()
         render = page.render_cache
     except (AttributeError, NotImplementedError):
-        return request.wiki.download(request, title)
+        return request.wiki.download(title)
 
     cache_dir = os.path.join(request.wiki.cache, 'render',
                               werkzeug.url_quote(title, safe=''))
@@ -328,8 +321,8 @@
     response.direct_passthrough = True
     return response
 
-@URL('/+undo/<title:title>', methods=['POST'])
-def undo(request, title):
+@views.route('/+undo/<title:title>', methods=['POST'])
+def undo(title):
     """Revert a change to a page."""
 
     _ = request.wiki.gettext
@@ -340,7 +333,7 @@
             rev = int(key)
         except ValueError:
             pass
-    author = request.get_author()
+    author = hatta.page.get_author()
     if rev is not None:
         try:
             parent = int(request.form.get("parent"))
@@ -359,12 +352,11 @@
             request.wiki.storage.save_data(title, data, author, comment, parent)
         page = hatta.page.get_page(request, title)
         request.wiki.index.update_page(page, title, data=data)
-    url = request.adapter.build('history', {'title': title},
-                                method='GET', force_external=True)
+    url = flask.url_for('hatta.history', title=title, method='GET', _external=True)
     return werkzeug.redirect(url, 303)
 
-@URL('/+history/<title:title>')
-def history(request, title):
+@views.route('/+history/<title:title>')
+def history(title):
     """Display history of changes of a page."""
 
     max_rev = -1
@@ -374,13 +366,13 @@
         if max_rev < rev:
             max_rev = rev
         if rev > 0:
-            date_url = request.adapter.build('diff', {
+            date_url = flask.url_for('hatta.diff', **{
                 'title': title,
                 'from_rev': rev - 1,
                 'to_rev': rev,
             })
         else:
-            date_url = request.adapter.build('revision', {
+            date_url = flask.url_for('hatta.revision', **{
                 'title': title,
                 'rev': rev,
             })
@@ -390,8 +382,8 @@
     response = hatta.response.response(request, title, html, '/history')
     return response
 
-@URL('/+history/')
-def recent_changes(request):
+@views.route('/+history/')
+def recent_changes():
     """Serve the recent changes page."""
 
     def _changes_list():
@@ -405,18 +397,18 @@
             if count > 100:
                 break
             if rev > 0:
-                date_url = request.adapter.build('diff', {
+                date_url = flask.url_for('hatta.diff', **{
                     'title': title,
                     'from_rev': rev - 1,
                     'to_rev': lastrev.get(title, rev),
                 })
             elif rev == 0:
-                date_url = request.adapter.build('revision', {
+                date_url = flask.url_for('hatta.revision', **{
                     'title': title,
                     'rev': rev,
                 })
             else:
-                date_url = request.adapter.build('history', {'title': title})
+                date_url = flask.url_for('hatta.history', **{'title': title})
             last[title] = author, comment
             lastrev[title] = rev
 
@@ -430,8 +422,8 @@
     response.make_conditional(request)
     return response
 
-@URL('/+history/<title:title>/<int:from_rev>:<int:to_rev>')
-def diff(request, title, from_rev, to_rev):
+@views.route('/+history/<title:title>/<int:from_rev>:<int:to_rev>')
+def diff(title, from_rev, to_rev):
     """Show the differences between specified revisions."""
 
     _ = request.wiki.gettext
@@ -462,8 +454,8 @@
     response = hatta.response.WikiResponse(html, mimetype='text/html')
     return response
 
-@URL('/+index')
-def all_pages(request):
+@views.route('/+index')
+def all_pages():
     """Show index of all pages in the request.wiki."""
 
     _ = request.wiki.gettext
@@ -478,8 +470,8 @@
     response.make_conditional(request)
     return response
 
-@URL('/+sister-index')
-def sister_pages(request):
+@views.route('/+sister-index')
+def sister_pages():
     """Show index of all pages in a format suitable for SisterPages."""
 
     text = [
@@ -492,8 +484,8 @@
     response.make_conditional(request)
     return response
 
-@URL('/+orphaned')
-def orphaned(request):
+@views.route('/+orphaned')
+def orphaned():
     """Show all pages that don't have backlinks."""
 
     _ = request.wiki.gettext
@@ -513,8 +505,8 @@
     response.make_conditional(request)
     return response
 
-@URL('/+wanted')
-def wanted(request):
+@views.route('/+wanted')
+def wanted():
     """Show all pages that don't exist yet, but are linked."""
 
     def _wanted_pages_list():
@@ -530,8 +522,8 @@
     response.make_conditional(request)
     return response
 
-@URL('/+search', methods=['GET', 'POST'])
-def search(request):
+@views.route('/+search', methods=['GET', 'POST'])
+def search():
     """Serve the search results page."""
 
     _ = request.wiki.gettext
@@ -563,8 +555,10 @@
         request.wiki.storage.reopen()
         request.wiki.index.update(request.wiki)
         result = sorted(request.wiki.index.find(words), key=lambda x: -x[0])
-        yield werkzeug.html.p(h(_(u'%d page(s) containing all words:')
-                              % len(result)))
+        yield werkzeug.html.p(h(_(
+            u'%(num)d page(s) containing all words:',
+            num=len(result)
+        )))
         yield u'<ol id="hatta-search-results">'
         for number, (score, title) in enumerate(result):
             yield h.li(h.b(page.wiki_link(title)), u' ', h.i(str(score)),
@@ -581,14 +575,14 @@
     words = tuple(request.wiki.index.split_text(query))
     if not words:
         words = (query,)
-    title = _(u'Searching for "%s"') % u" ".join(words)
+    title = _(u'Searching for "%(words)s"', words=u" ".join(words))
     content = page_search(words, page, request)
     html = page.template('page_special.html', content=content,
                          special_title=title)
     return hatta.response.WikiResponse(html, mimetype='text/html')
 
-@URL('/+search/<title:title>', methods=['GET', 'POST'])
-def backlinks(request, title):
+@views.route('/+search/<title:title>', methods=['GET', 'POST'])
+def backlinks(title):
     """Serve the page with backlinks."""
 
     request.wiki.storage.reopen()
@@ -601,22 +595,22 @@
     response.make_conditional(request)
     return response
 
-@URL('/+download/scripts.js')
-def scripts_js(request):
+@views.route('/+download/scripts.js')
+def scripts_js():
     """Server the default scripts"""
 
-    return _serve_default(request, 'scripts.js',
+    return _serve_default('scripts.js',
                                mime='text/javascript')
 
-@URL('/+download/style.css')
-def style_css(request):
+@views.route('/+download/style.css')
+def style_css():
     """Serve the default style"""
 
-    return _serve_default(request, 'style.css',
+    return _serve_default('style.css',
                                mime='text/css')
 
-@URL('/+download/pygments.css')
-def pygments_css(request):
+@views.route('/+download/pygments.css')
+def pygments_css():
     """Serve the default pygments style"""
 
     _ = request.wiki.gettext
@@ -629,25 +623,25 @@
         pygments_style = 'default'
     formatter = pygments.formatters.HtmlFormatter(style=pygments_style)
     style_defs = formatter.get_style_defs('.highlight')
-    return _serve_default(request, 'pygments.css', style_defs,
+    return _serve_default('pygments.css', style_defs,
                                'text/css')
 
-@URL('/favicon.ico')
-def favicon_ico(request):
+@views.route('/favicon.ico')
+def favicon_ico():
     """Serve the default favicon."""
 
-    return _serve_default(request, 'favicon.ico',
+    return _serve_default('favicon.ico',
                                mime='image/x-icon')
 
-@URL('/robots.txt')
-def robots_txt(request):
+@views.route('/robots.txt')
+def robots_txt():
     """Serve the robots directives."""
 
-    return _serve_default(request, 'robots.txt',
+    return _serve_default('robots.txt',
                                mime='text/plain')
 
-@URL('/+hg<all:path>', methods=['GET', 'POST', 'HEAD'])
-def hgweb(request, path=None):
+@views.route('/+hg<all:path>', methods=['GET', 'POST', 'HEAD'])
+def hgweb(path=None):
     """
     Serve the pages repository on the web like a normal hg repository.
     """
@@ -667,8 +661,8 @@
         return app(env, start)
     return hg_app
 
-@URL('/off-with-his-head', methods=['GET'])
-def die(request):
+@views.route('/off-with-his-head', methods=['GET'])
+def die():
     """Terminate the standalone server if invoked from localhost."""
 
     _ = request.wiki.gettext
--- a/hatta/wiki.py	Sat May 18 16:10:36 2013 +0200
+++ b/hatta/wiki.py	Sun May 19 13:16:35 2013 +0200
@@ -1,67 +1,19 @@
 #!/usr/bin/python
 # -*- coding: utf-8 -*-
 
-import gettext
 import os
 import sys
 
-import werkzeug
-import werkzeug.routing
-import jinja2
+import flaskext.babel
 
 import hatta.error
 import hatta.page
 import hatta.search
 import hatta.storage
 import hatta.views
-import hatta.request
 import hatta.response
 
 
-class WikiTitleConverter(werkzeug.routing.PathConverter):
-    """Behaves like the path converter, but doesn't match the "+ pages"."""
-
-    def to_url(self, value):
-        return werkzeug.url_quote(value.strip(), self.map.charset, safe="/")
-
-    regex = '([^+%]|%[^2]|%2[^Bb]).*'
-
-
-class WikiAllConverter(werkzeug.routing.BaseConverter):
-    """Matches everything."""
-
-    regex = '.*'
-
-
-def init_gettext(language):
-    if language is not None:
-        try:
-            translation = gettext.translation(
-                'hatta',
-                'locale',
-                languages=[language],
-            )
-        except IOError:
-            translation = gettext.translation(
-                'hatta',
-                fallback=True,
-                languages=[language],
-            )
-    else:
-        translation = gettext.translation('hatta', fallback=True)
-    return translation
-
-
-def init_template(translation):
-    template_env = jinja2.Environment(
-        extensions=['jinja2.ext.i18n'],
-        loader=jinja2.PackageLoader('hatta', 'templates'),
-    )
-    template_env.autoescape = True
-    template_env.install_gettext_translations(translation, True)
-    return template_env
-
-
 class Wiki(object):
     """
     The main class of the wiki, handling initialization of the whole
@@ -80,9 +32,8 @@
         self.config = config
 
         self.language = config.get('language')
-        translation = init_gettext(self.language)
-        self.gettext = translation.ugettext
-        self.template_env = init_template(translation)
+        self.gettext = flaskext.babel.gettext
+        self.ngettext = flaskext.babel.ngettext
         self.path = os.path.abspath(config.get('pages_path', 'docs'))
         self.repo_path = config.get('repo_path')
         self.page_charset = config.get('page_charset', 'utf-8')
@@ -124,37 +75,4 @@
         )
         self.index = self.index_class(self.cache, self.language, self.storage)
         self.index.update(self)
-        self.url_rules = hatta.views.URL.get_rules()
-        self.views = hatta.views.URL.get_views()
-        self.url_converters = {
-            'title': WikiTitleConverter,
-            'all': WikiAllConverter,
-        }
-        self.url_map = werkzeug.routing.Map(
-            self.url_rules,
-            converters=self.url_converters,
-        )
 
-    def add_url_rule(self, rule, name, func):
-        """Let plugins add additional url rules."""
-
-        self.url_rules.append(rule)
-        self.views[name] = func
-        self.url_map = werkzeug.routing.Map(
-            self.url_rules,
-            converters=self.url_converters,
-        )
-
-    @werkzeug.responder
-    def application(self, environ, start):
-        """The main application loop."""
-
-        adapter = self.url_map.bind_to_environ(environ)
-        request = hatta.request.WikiRequest(self, adapter, environ)
-        try:
-            endpoint, values = adapter.match()
-            view = self.views[endpoint]
-            return view(request, **values)
-        except werkzeug.exceptions.HTTPException as err:
-            return err
-
--- a/setup.py	Sat May 18 16:10:36 2013 +0200
+++ b/setup.py	Sun May 19 13:16:35 2013 +0200
@@ -19,6 +19,8 @@
         'werkzeug >=0.3',
         'mercurial >=1.0',
         'jinja2',
+        'flask',
+        'flask-babel',
 #        'pygments',
     ],
     tests_require=['py.test', 'lxml', 'pygments'],
--- a/tests/test_hatta.py	Sat May 18 16:10:36 2013 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,289 +0,0 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-import werkzeug
-import os
-import py.test
-import lxml.doctestcompare
-from test_parser import HTML
-
-import hatta
-
-
-def clear_directory(top):
-    for root, dirs, files in os.walk(top, topdown=False):
-        for name in files:
-            os.remove(os.path.join(root, name))
-        for name in dirs:
-            os.rmdir(os.path.join(root, name))
-    try:
-        os.removedirs(top)
-    except OSError:
-        pass
-
-def pytest_funcarg__wiki(request):
-    basedir = str(py.test.ensuretemp('repo'))
-    config = hatta.WikiConfig(
-        pages_path=os.path.join(basedir, 'pages'),
-        cache_path=os.path.join(basedir, 'cache'),
-    )
-    request.addfinalizer(lambda: clear_directory(basedir))
-    return hatta.Wiki(config)
-
-
-class TestHattaStandalone(object):
-    docstring = '''<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
-"http://www.w3.org/TR/html4/strict.dtd">'''
-
-
-    def test_japanese_splitting(self, wiki):
-        text = u"ルビハイパンツアクセシウェブ内容アテストスイトどらプロセスドクリック」インタラクションディア,情報セットセシビリティングシステムをマその他リア式会を始めてみようサイトをアクセシブ内准剛のな,健二仕ルビの再形式化セシビリテのためらすかるコンテンウェブ内容アネッユザエクアップテキストマでの,ネックセスふべからずビリティにるその他クアップコンテンツアクセネッ"
-        after = [u'ルビハイパンツアクセシウェブ', u'内容', u'アテストスイト', u'どら', u'プロセスドクリック', u'インタラクションディア', u'情報', u'セットセシビリティングシステム', u'を', u'マ', u'その', u'他', u'リア', u'式会', u'を', u'始', u'めてみよう', u'サイト', u'を', u'アクセシブ', u'内准剛', u'のな', u'健二仕', u'ルビ', u'の', u'再形式化', u'セシビリテ', u'のためらすかる', u'コンテンウェブ', u'内容', u'アネッユザエクアップテキストマ', u'での', u'ネックセス', u'ふべからず', u'ビリティ', u'にるその', u'他', u'クアップコンテンツアクセネッ']
-        result = list(wiki.index.split_japanese_text(text))
-        for got, expected in zip(result, after):
-            assert got == expected
-
-    def test_front_page(self, wiki):
-        """Check that Home page doesn't exist and redirects to editor."""
-
-        client = werkzeug.Client(wiki.application, hatta.WikiResponse)
-        response = client.get('')
-        assert response.status_code == 303
-        assert response.headers['Location'] in (
-            'http://localhost/+edit/Home',
-            'http://localhost/%2Bedit/Home',
-        )
-        response = client.get('/+edit/Home')
-        assert response.status_code == 404
-
-    def test_create_front_page(self, wiki):
-        """Create a Home page and make sure it's created propely."""
-
-        client = werkzeug.Client(wiki.application, hatta.WikiResponse)
-        data = 'text=test&parent=-1&comment=created&author=test&save=Save'
-        response = client.post('/+edit/Home', data=data, content_type='application/x-www-form-urlencoded')
-        assert response.status_code == 303
-        response = client.get('')
-        assert response.status_code == 200
-
-    def test_page_docstring(self, wiki):
-        """Check the page's docstring."""
-
-        client = werkzeug.Client(wiki.application, hatta.WikiResponse)
-        data = 'text=test&parent=-1&comment=created&author=test&save=Save'
-        response = client.post('/+edit/Home', data=data,
-                            content_type='application/x-www-form-urlencoded')
-        assert response.status_code == 303
-        response = client.get('')
-        assert response.status_code == 200
-        data = ''.join(response.data)
-        assert data.startswith(self.docstring)
-
-    def test_editor_docstring(self, wiki):
-        """Check the editor's docstring."""
-
-        client = werkzeug.Client(wiki.application, hatta.WikiResponse)
-        response = client.get('/+edit/Home')
-        data = ''.join(response.data)
-        assert data.startswith(self.docstring)
-
-    def test_create_slash_page(self, wiki):
-        """Create a page with slash in name."""
-
-        client = werkzeug.Client(wiki.application, hatta.WikiResponse)
-        data = 'text=test&parent=-1&comment=created&author=test&save=Save'
-        response = client.post('/+edit/1/2', data=data,
-                            content_type='application/x-www-form-urlencoded')
-        assert response.status_code == 303
-        response = client.get('/1/2')
-        assert response.status_code == 200
-        response = client.get('/+history/1/2/0')
-        assert response.status_code == 200
-
-    def test_search(self, wiki):
-        """Test simple searching."""
-
-        client = werkzeug.Client(wiki.application, hatta.WikiResponse)
-        data = 'text=test&parent=-1&comment=created&author=test&save=Save'
-        response = client.post('/+edit/searching', data=data,
-                            content_type='application/x-www-form-urlencoded')
-        assert response.status_code == 303
-        response = client.get('/+search?q=test')
-        assert response.status_code == 200
-        data = ''.join(response.data)
-        assert '>searching</a>' in data
-
-    def test_read_only_edit(self, wiki):
-        client = werkzeug.Client(wiki.application, hatta.WikiResponse)
-        wiki.read_only = True
-        data = 'text=test&parent=-1&comment=created&author=test&save=Save'
-        response = client.post('/+edit/readonly', data=data,
-                            content_type='application/x-www-form-urlencoded')
-        assert response.status_code == 403
-
-    def test_read_only_undo(self, wiki):
-        client = werkzeug.Client(wiki.application, hatta.WikiResponse)
-        wiki.read_only = True
-        data = '52=Undo'
-        response = client.post('/+undo/readonly', data=data,
-                            content_type='application/x-www-form-urlencoded')
-        assert response.status_code == 403
-
-
-class TestHattaParser(object):
-
-    def parse_text(self, text):
-        parser = hatta.parser.WikiParser
-        def link(addr, label=None, class_=None, image=None, alt=None):
-            return u"<a></a>"
-        def image(addr, label=None, class_=None, image=None, alt=None):
-            return u"<img>"
-        return u''.join(parser(text.split('\n'), link, image))
-
-
-    test_cases = {
-u"""hello world""": u"""<p id="line_0">hello world</p>""",
-#--------------------------------------------------------------------
-u"""{{{
-some code
-more
-}}}
-some text
-{{{
-more code
-}}}""": u"""<pre class="code" id="line_1">some code
-more</pre><p id="line_4">some text</p><pre class="code" id="line_6">more code</pre>""",
-#--------------------------------------------------------------------
-u"""{{{#!python
-some code
-more
-}}}
-some text
-{{{#!bash
-more code
-}}}""": u"""<div class="highlight"><pre id="line_1">some code
-more</pre></div><p id="line_4">some text</p><div class="highlight"><pre id="line_6">more code</pre></div>""",
-#--------------------------------------------------------------------
-u"""Here's a quote:
-> Here is
-> another //quote//:
->> A quote **within
->> a quote
-normal text""": u"""<p id="line_0">Here's a quote:</p><blockquote><p id="line_1">Here is
-another <i>quote</i>:</p><blockquote><p id="line_3">A quote <b>within
-a quote</b></p></blockquote></blockquote><p id="line_5">normal text</p>""",
-#--------------------------------------------------------------------
-u"""* sample list
-** sublist
-*** sub-sub-list with **bold
-* list""": u"""<ul id="line_0"><li>sample list<ul id="line_1"><li>sublist<ul id="line_2"><li>sub-sub-list with <b>bold</b></li></ul></li></ul></li><li>list</li></ul>""",
-}
-
-    def test_test_cases(self):
-        for text, expect in self.test_cases.iteritems():
-            assert expect == self.parse_text(text)
-
-
-def pytest_funcarg__req(request):
-    basedir = str(py.test.ensuretemp('repo'))
-    request.addfinalizer(lambda: clear_directory(basedir))
-    config = hatta.WikiConfig(
-        pages_path=os.path.join(basedir, 'pages'),
-        cache_path=os.path.join(basedir, 'cache'),
-        default_style="...",
-    )
-    wiki = hatta.Wiki(config)
-    environ = {
-        'SERVER_NAME': 'hatta',
-        'wsgi.url_scheme': 'http',
-        'SERVER_PORT': '80',
-        'REQUEST_METHOD': 'GET',
-        'PATH_INFO': '/',
-        'SCRIPT_NAME': '',
-    }
-    adapter = wiki.url_map.bind_to_environ(environ)
-    return wiki, hatta.WikiRequest(wiki, adapter, environ)
-
-class TestHTML(object):
-    def test_wiki_request_get_url(self, req):
-        wiki, request = req
-        assert request.get_url('title') == u'/title'
-        assert request.get_download_url('title') in (
-            u'/+download/title',
-            u'/%2Bdownload/title',
-        )
-        assert request.get_url('title', 'edit') in (
-            u'/+edit/title',
-            u'/%2Bedit/title',
-        )
-        assert request.get_url(None, 'favicon_ico') == u'/favicon.ico'
-
-    @py.test.mark.xfail
-    def test_html_page(self, req):
-        wiki, request = req
-        content = ["some &lt;content&gt;"]
-        title = "page <title>"
-        page = wiki.get_page(request, title)
-        parts = page.render_content(content)
-        html = HTML(u"".join(parts))
-        assert html == u"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
-"http://www.w3.org/TR/html4/strict.dtd">
-<html><head>
-    <title>page &lt;title&gt; - Hatta Wiki</title>
-    <link type="text/css" href="/+download/pygments.css" rel="stylesheet">
-    <link type="text/css" href="/+download/style.css" rel="stylesheet">
-    <link type="application/wiki" href="/+edit/page%20%3Ctitle%3E" rel="alternate">
-    <link type="image/x-icon" href="/favicon.ico" rel="shortcut icon">
-    <link type="application/rss+xml" href="/+feed/atom" rel="alternate" title="Hatta Wiki (ATOM)">
-    <script src="/+download/scripts.js" type="text/javascript"></script>
-</head><body>
-    <div class="header">
-        <form action="/+search" class="search" method="GET"><div>
-            <input class="search" name="q">
-            <input class="button" type="submit" value="Search">
-        </div></form>
-        <div class="menu">
-          <a href="/Home" title="Home" class="wiki nonexistent">Home</a>
-          <a href="/+history" title="+history" class="special">Recent changes</a>
-        </div>
-        <h1>page &lt;title&gt;</h1>
-    </div>
-    <div class="content">some &lt;content&gt;
-    <div class="footer">
-        <a href="/+edit/page%20%3Ctitle%3E" class="edit">Edit</a>
-        <a href="/+history/page%20%3Ctitle%3E" class="history">History</a>
-        <a href="/+search/page%20%3Ctitle%3E" class="backlinks">Backlinks</a>
-    </div></div>
-</body></html>"""
-
-        page_title = "different <title>"
-        page = wiki.get_page(request, title)
-        parts = page.render_content(content, page_title)
-        html = HTML(u"".join(parts))
-        assert html == u"""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
-"http://www.w3.org/TR/html4/strict.dtd">
-<html><head>
-    <title>different &lt;title&gt; - Hatta Wiki</title>
-    <link type="text/css" href="/+download/pygments.css" rel="stylesheet">
-    <link type="text/css" href="/+download/style.css" rel="stylesheet">
-    <meta content="NOINDEX,NOFOLLOW" name="robots">
-    <link type="image/x-icon" href="/favicon.ico" rel="shortcut icon">
-    <link type="application/rss+xml" href="/+feed/atom" rel="alternate" title="Hatta Wiki (ATOM)">
-    <script src="/+download/scripts.js" type="text/javascript"></script>
-</head><body>
-    <div class="header">
-        <form action="/+search" class="search" method="GET"><div>
-            <input class="search" name="q">
-            <input class="button" type="submit" value="Search">
-        </div></form>
-        <div class="menu">
-          <a href="/Home" title="Home" class="wiki nonexistent">Home</a>
-          <a href="/+history" title="+history" class="special">Recent changes</a>
-        </div>
-        <h1>different &lt;title&gt;</h1>
-    </div>
-    <div class="content">some &lt;content&gt;
-       </div>
-</body></html>"""
-
-