changeset 1832:ff503e7ea7a0 namespaces

merged default into namespaces branch, some stuff XXX BROKEN
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sat, 06 Oct 2012 17:46:24 +0200
parents 7657798d40f3 (current diff) 9e8e47fd8fba (diff)
children 1ac84f17c5c1
files .hgignore MoinMoin/_tests/__init__.py MoinMoin/_tests/pep8.py MoinMoin/_tests/test_sourcecode.py MoinMoin/_tests/test_test_environ.py MoinMoin/_tests/test_user.py MoinMoin/app.py MoinMoin/apps/admin/templates/admin/highlighterhelp.html MoinMoin/apps/admin/templates/admin/interwikihelp.html MoinMoin/apps/admin/templates/admin/itemsize.html MoinMoin/apps/admin/views.py MoinMoin/apps/feed/views.py MoinMoin/apps/frontend/views.py MoinMoin/apps/misc/views.py MoinMoin/auth/_tests/test_auth.py MoinMoin/auth/_tests/test_http.py MoinMoin/auth/_tests/test_log.py MoinMoin/auth/ldap_login.py MoinMoin/config/default.py MoinMoin/conftest.py MoinMoin/constants/keys.py MoinMoin/datastruct/backends/_tests/test_wiki_groups.py MoinMoin/items/__init__.py MoinMoin/items/_tests/test_Item.py MoinMoin/items/blog.py MoinMoin/items/content.py MoinMoin/script/account/create.py MoinMoin/script/maint/moinshell.py MoinMoin/script/maint/serialization.py MoinMoin/script/migration/moin19/import19.py MoinMoin/security/__init__.py MoinMoin/security/_tests/test_security.py MoinMoin/static/js/common.js MoinMoin/static/js/countdown.js MoinMoin/static/js/index_action.js MoinMoin/storage/__init__.py MoinMoin/storage/backends/stores.py MoinMoin/storage/middleware/_tests/test_indexing.py MoinMoin/storage/middleware/_tests/test_protecting.py MoinMoin/storage/middleware/_tests/test_routing.py MoinMoin/storage/middleware/_tests/test_serialization.py MoinMoin/storage/middleware/_tests/test_validation.py MoinMoin/storage/middleware/indexing.py MoinMoin/storage/middleware/protecting.py MoinMoin/storage/middleware/routing.py MoinMoin/storage/middleware/serialization.py MoinMoin/storage/middleware/validation.py MoinMoin/templates/forms.html MoinMoin/templates/global_history.html MoinMoin/templates/history.html MoinMoin/templates/layout.html MoinMoin/templates/modify_applet.html MoinMoin/templates/modify_show_template_selection.html MoinMoin/templates/modify_show_type_selection.html MoinMoin/templates/search.html MoinMoin/templates/usersettings.html MoinMoin/templates/usersettings_forms.html MoinMoin/themes/__init__.py MoinMoin/themes/foobar/static/css/stylus/stylus_notice.css MoinMoin/themes/foobar/templates/delete.html MoinMoin/themes/foobar/templates/destroy.html MoinMoin/themes/foobar/templates/global.html MoinMoin/themes/foobar/templates/global_history.html MoinMoin/themes/foobar/templates/global_tags.html MoinMoin/themes/foobar/templates/history.html MoinMoin/themes/foobar/templates/item_link_list.html MoinMoin/themes/foobar/templates/itemviews.html MoinMoin/themes/foobar/templates/local.html MoinMoin/themes/foobar/templates/login.html MoinMoin/themes/foobar/templates/lostpass.html MoinMoin/themes/foobar/templates/meta.html MoinMoin/themes/foobar/templates/modify_applet.html MoinMoin/themes/foobar/templates/modify_binary.html MoinMoin/themes/foobar/templates/modify_show_template_selection.html MoinMoin/themes/foobar/templates/modify_show_type_selection.html MoinMoin/themes/foobar/templates/modify_text.html MoinMoin/themes/foobar/templates/modify_text_html.html MoinMoin/themes/foobar/templates/recoverpass.html MoinMoin/themes/foobar/templates/register.html MoinMoin/themes/foobar/templates/rename.html MoinMoin/themes/foobar/templates/sitemap.html MoinMoin/translations/MoinMoin.pot MoinMoin/user.py MoinMoin/util/_tests/test_interwiki.py MoinMoin/util/_tests/test_lock.py MoinMoin/util/interwiki.py MoinMoin/util/lock.py wikiconfig.py
diffstat 499 files changed, 14795 insertions(+), 10332 deletions(-) [+]
line wrap: on
line diff
--- a/.hgignore	Tue Mar 27 23:48:23 2012 +0200
+++ b/.hgignore	Sat Oct 06 17:46:24 2012 +0200
@@ -26,3 +26,7 @@
 .orig$
 .rej$
 .~$
+^docs/devel/api/modules.rst
+^upload.py
+^build/
+\..*sw[op]$
--- a/Makefile	Tue Mar 27 23:48:23 2012 +0200
+++ b/Makefile	Sat Oct 06 17:46:24 2012 +0200
@@ -8,6 +8,9 @@
 all:
 	python setup.py build
 
+test:
+	py.test --pep8 -rs
+
 dist: clean-devwiki
 	-rm MANIFEST
 	python setup.py sdist
@@ -23,9 +26,6 @@
 	wget -U MoinMoin/Makefile -O contrib/interwiki/intermap.txt "http://master19.moinmo.in/InterWikiMap?action=raw"
 	chmod 664 contrib/interwiki/intermap.txt
 
-check-tabs:
-	@python -c 'import tabnanny ; tabnanny.check("MoinMoin")'
-
 pylint:
 	@pylint --disable-msg=W0142,W0511,W0612,W0613,C0103,C0111,C0302,C0321,C0322 --disable-msg-cat=R MoinMoin
 
--- a/MoinMoin/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -19,4 +19,3 @@
 from MoinMoin.util.version import Version
 
 version = Version(2, 0, 0, 'a0')
-
--- a/MoinMoin/_template.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/_template.py	Sat Oct 06 17:46:24 2012 +0200
@@ -16,4 +16,3 @@
 
 
 from __future__ import absolute_import, division
-
--- a/MoinMoin/_tests/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/_tests/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -52,6 +52,7 @@
         data = data.encode(config.charset)
     item = flaskg.storage[name]
 
+    meta = meta.copy()
     if NAME not in meta:
         meta[NAME] = [name, ]
     if CONTENTTYPE not in meta:
@@ -82,5 +83,3 @@
         s.close()
     except socket.error as err:
         raise Exception("connecting to {0}:{1:d}, error: {2!s}".format(host, port, err))
-
-
--- a/MoinMoin/_tests/_test_template.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/_tests/_test_template.py	Sat Oct 06 17:46:24 2012 +0200
@@ -40,8 +40,8 @@
     can add a test by adding another line to this list
     """
     _tests = (
-        # description,  test,            expected
-        ('Line break',  '<<BR>>',        '<br>'),
+        # description, test, expected
+        ('Line break', '<<BR>>', '<br>'),
     )
 
     from MoinMoin._tests import wikiconfig
@@ -71,4 +71,3 @@
         module_tested.do_that()
         result = None
         return result
-
--- a/MoinMoin/_tests/ldap_testbase.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/_tests/ldap_testbase.py	Sat Oct 06 17:46:24 2012 +0200
@@ -258,4 +258,3 @@
 
 except ImportError:
     pass  # obviously pytest not in use
-
--- a/MoinMoin/_tests/ldap_testdata.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/_tests/ldap_testdata.py	Sat Oct 06 17:46:24 2012 +0200
@@ -116,4 +116,3 @@
 member: cn=Group A,ou=Groups,ou=testing,dc=example,dc=org
 objectClass: groupOfNames
 """
-
--- a/MoinMoin/_tests/pep8.py	Tue Mar 27 23:48:23 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,916 +0,0 @@
-#!/usr/bin/python
-# pep8.py - Check Python source code formatting, according to PEP 8
-# Copyright (C) 2006 Johann C. Rocholl <johann@browsershots.org>
-#
-# Permission is hereby granted, free of charge, to any person
-# obtaining a copy of this software and associated documentation files
-# (the "Software"), to deal in the Software without restriction,
-# including without limitation the rights to use, copy, modify, merge,
-# publish, distribute, sublicense, and/or sell copies of the Software,
-# and to permit persons to whom the Software is furnished to do so,
-# subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
-# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
-# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-# SOFTWARE.
-
-"""
-Check Python source code formatting, according to PEP 8:
-http://www.python.org/dev/peps/pep-0008/
-
-For usage and a list of options, try this:
-$ python pep8.py -h
-
-This program and its regression test suite live here:
-http://svn.browsershots.org/trunk/devtools/pep8/
-http://trac.browsershots.org/browser/trunk/devtools/pep8/
-
-Groups of errors and warnings:
-E errors
-W warnings
-100 indentation
-200 whitespace
-300 blank lines
-400 imports
-500 line length
-600 deprecation
-700 statements
-
-You can add checks to this program by writing plugins. Each plugin is
-a simple function that is called for each line of source code, either
-physical or logical.
-
-Physical line:
-- Raw line of text from the input file.
-
-Logical line:
-- Multi-line statements converted to a single line.
-- Stripped left and right.
-- Contents of strings replaced with 'xxx' of same length.
-- Comments removed.
-
-The check function requests physical or logical lines by the name of
-the first argument:
-
-def maximum_line_length(physical_line)
-def extraneous_whitespace(logical_line)
-def blank_lines(logical_line, blank_lines, indent_level, line_number)
-
-The last example above demonstrates how check plugins can request
-additional information with extra arguments. All attributes of the
-Checker object are available. Some examples:
-
-lines: a list of the raw lines from the input file
-tokens: the tokens that contribute to this logical line
-line_number: line number in the input file
-blank_lines: blank lines before this one
-indent_char: first indentation character in this file (' ' or '\t')
-indent_level: indentation (with tabs expanded to multiples of 8)
-previous_indent_level: indentation on previous line
-previous_logical: previous logical line
-
-The docstring of each check function shall be the relevant part of
-text from PEP 8. It is printed if the user enables --show-pep8.
-
-"""
-
-import os
-import sys
-import re
-import time
-import inspect
-import tokenize
-from optparse import OptionParser
-from keyword import iskeyword
-from fnmatch import fnmatch
-
-__version__ = '0.2.0'
-__revision__ = '$Rev$'
-
-default_exclude = '.svn,CVS,*.pyc,*.pyo'
-
-indent_match = re.compile(r'([ \t]*)').match
-raise_comma_match = re.compile(r'raise\s+\w+\s*(,)').match
-equals_boolean_search = re.compile(r'([!=]=\s*(True|False))|((True|False)\s*[!=]=)').search
-equals_None_search = re.compile(r'([!=]=\s*None)|(None\s*[!=]=)').search
-
-not_one_ws_around_operators_match = re.compile(r'^[^\(\[]+[^\s](\+|\-|\*|/|%|\^|&|\||=|<|>|>>|<<|\+=|\-=|\*=|/=|%=|\^=|&=|\|=|==|<=|>=|>>=|<<=|!=|<>)[^\s][^\)\]]+$').match
-
-operators = """
-+  -  *  /  %  ^  &  |  =  <  >  >>  <<
-+= -= *= /= %= ^= &= |= == <= >= >>= <<=
-!= <> :
-in is or not and
-""".split()
-
-options = None
-args = None
-
-
-##############################################################################
-# Plugins (check functions) for physical lines
-##############################################################################
-
-
-def tabs_or_spaces(physical_line, indent_char):
-    """
-    Never mix tabs and spaces.
-
-    The most popular way of indenting Python is with spaces only.  The
-    second-most popular way is with tabs only.  Code indented with a mixture
-    of tabs and spaces should be converted to using spaces exclusively.  When
-    invoking the Python command line interpreter with the -t option, it issues
-    warnings about code that illegally mixes tabs and spaces.  When using -tt
-    these warnings become errors.  These options are highly recommended!
-    """
-    indent = indent_match(physical_line).group(1)
-    for offset, char in enumerate(indent):
-        if char != indent_char:
-            return offset, "E101 indentation contains mixed spaces and tabs"
-
-
-def tabs_obsolete(physical_line):
-    """
-    For new projects, spaces-only are strongly recommended over tabs.  Most
-    editors have features that make this easy to do.
-    """
-    indent = indent_match(physical_line).group(1)
-    if indent.count('\t'):
-        return indent.index('\t'), "W191 indentation contains tabs"
-
-
-def trailing_whitespace(physical_line):
-    """
-    JCR: Trailing whitespace is superfluous.
-    """
-    physical_line = physical_line.rstrip('\n') # chr(10), newline
-    physical_line = physical_line.rstrip('\r') # chr(13), carriage return
-    physical_line = physical_line.rstrip('\x0c') # chr(12), form feed, ^L
-    stripped = physical_line.rstrip()
-    if physical_line != stripped:
-        return len(stripped), "W291 trailing whitespace"
-
-
-def trailing_blank_lines(physical_line, lines, line_number):
-    """
-    JCR: Trailing blank lines are superfluous.
-    """
-    if physical_line.strip() == '' and line_number == len(lines):
-        return 0, "W391 blank line at end of file"
-
-
-def missing_newline(physical_line):
-    """
-    JCR: The last line should have a newline.
-    """
-    if physical_line.rstrip() == physical_line:
-        return len(physical_line), "W292 no newline at end of file"
-
-
-def maximum_line_length(physical_line):
-    """
-    Limit all lines to a maximum of 79 characters.
-
-    There are still many devices around that are limited to 80 character
-    lines; plus, limiting windows to 80 characters makes it possible to have
-    several windows side-by-side.  The default wrapping on such devices looks
-    ugly.  Therefore, please limit all lines to a maximum of 79 characters.
-    For flowing long blocks of text (docstrings or comments), limiting the
-    length to 72 characters is recommended.
-    """
-    length = len(physical_line.rstrip())
-    if length > 79:
-        return 79, "E501 line too long (%d characters)" % length
-
-
-def crlf_lines(physical_line):
-    """
-    Line contains CR (e.g. as a CRLF line ending).
-
-    Many free software projects have a strong focus on POSIX platforms (like
-    Linux, *BSD, Unix, Mac OS X, etc.) and they all use LF-only line endings.
-    Only Win32 platform uses CRLF line endings.
-    So if you have a Win32-only source code using CRLF line endings, you might
-    want to exclude this test.
-    """
-    pos = physical_line.find('\r')
-    if pos >= 0:
-        return pos, "W293 line contains CR char(s)"
-
-
-##############################################################################
-# Plugins (check functions) for logical lines
-##############################################################################
-
-
-def blank_lines(logical_line, blank_lines, indent_level, line_number,
-                previous_logical):
-    """
-    Separate top-level function and class definitions with two blank lines.
-
-    Method definitions inside a class are separated by a single blank line.
-
-    Extra blank lines may be used (sparingly) to separate groups of related
-    functions.  Blank lines may be omitted between a bunch of related
-    one-liners (e.g. a set of dummy implementations).
-
-    Use blank lines in functions, sparingly, to indicate logical sections.
-    """
-    if line_number == 1:
-        return # Don't expect blank lines before the first line
-    if previous_logical.startswith('@'):
-        return # Don't expect blank lines after function decorator
-    if (logical_line.startswith('def ') or
-        logical_line.startswith('class ') or
-        logical_line.startswith('@')):
-        if indent_level > 0 and blank_lines != 1:
-            return 0, "E301 expected 1 blank line, found %d" % blank_lines
-        if indent_level == 0 and blank_lines != 2:
-            return 0, "E302 expected 2 blank lines, found %d" % blank_lines
-    if blank_lines > 2:
-        return 0, "E303 too many blank lines (%d)" % blank_lines
-
-
-def extraneous_whitespace(logical_line):
-    """
-    Avoid extraneous whitespace in the following situations:
-
-    - Immediately inside parentheses, brackets or braces.
-
-    - Immediately before a comma, semicolon, or colon.
-    """
-    line = logical_line
-    for char in '([{':
-        found = line.find(char + ' ')
-        if found > -1:
-            return found + 1, "E201 whitespace after '%s'" % char
-    for char in '}])':
-        found = line.find(' ' + char)
-        if found > -1 and line[found - 1] != ',':
-            return found, "E202 whitespace before '%s'" % char
-    for char in ',;:':
-        found = line.find(' ' + char)
-        if found > -1:
-            return found, "E203 whitespace before '%s'" % char
-
-
-def missing_whitespace(logical_line):
-    """
-    JCR: Each comma, semicolon or colon should be followed by whitespace.
-    """
-    line = logical_line
-    for index in range(len(line) - 1):
-        char = line[index]
-        if char in ',;:' and line[index + 1] != ' ':
-            before = line[:index]
-            if char == ':' and before.count('[') > before.count(']'):
-                continue # Slice syntax, no space required
-            return index, "E231 missing whitespace after '%s'" % char
-
-
-def indentation(logical_line, previous_logical, indent_char,
-                indent_level, previous_indent_level):
-    """
-    Use 4 spaces per indentation level.
-
-    For really old code that you don't want to mess up, you can continue to
-    use 8-space tabs.
-    """
-    if indent_char == ' ' and indent_level % 4:
-        return 0, "E111 indentation is not a multiple of four"
-    indent_expect = previous_logical.endswith(':')
-    if indent_expect and indent_level <= previous_indent_level:
-        return 0, "E112 expected an indented block"
-    if indent_level > previous_indent_level and not indent_expect:
-        return 0, "E113 unexpected indentation"
-
-
-def whitespace_before_parameters(logical_line, tokens):
-    """
-    Avoid extraneous whitespace in the following situations:
-
-    - Immediately before the open parenthesis that starts the argument
-      list of a function call.
-
-    - Immediately before the open parenthesis that starts an indexing or
-      slicing.
-    """
-    prev_type = tokens[0][0]
-    prev_text = tokens[0][1]
-    prev_end = tokens[0][3]
-    for index in range(1, len(tokens)):
-        token_type, text, start, end, line = tokens[index]
-        if (token_type == tokenize.OP and
-            text in '([' and
-            start != prev_end and
-            prev_type == tokenize.NAME and
-            (index < 2 or tokens[index - 2][1] != 'class') and
-            (not iskeyword(prev_text))):
-            return prev_end, "E211 whitespace before '%s'" % text
-        prev_type = token_type
-        prev_text = text
-        prev_end = end
-
-
-def extra_whitespace_around_operator(logical_line):
-    """
-    Avoid extraneous whitespace in the following situations:
-
-    - More than one space around an assignment (or other) operator to
-      align it with another.
-    """
-    line = logical_line
-    for operator in operators:
-        found = line.find('  ' + operator)
-        if found > -1:
-            return found, "E221 multiple spaces before operator"
-        found = line.find(operator + '  ')
-        if found > -1:
-            return found, "E222 multiple spaces after operator"
-        found = line.find('\t' + operator)
-        if found > -1:
-            return found, "E223 tab before operator"
-        found = line.find(operator + '\t')
-        if found > -1:
-            return found, "E224 tab after operator"
-
-
-def whitespace_around_operator(logical_line):
-    """
-    Have exactly 1 space left and right of the operator.
-    """
-    match = not_one_ws_around_operators_match(logical_line)
-    if match and not 'lambda' in logical_line:
-        return match.start(1), "E225 operators shall be surrounded by a single space on each side %s" % logical_line
-
-
-def whitespace_around_comma(logical_line):
-    """
-    Avoid extraneous whitespace in the following situations:
-
-    - More than one space around an assignment (or other) operator to
-      align it with another.
-
-    JCR: This should also be applied around comma etc.
-    """
-    line = logical_line
-    for separator in ',;:':
-        found = line.find(separator + '  ')
-        if found > -1:
-            return found + 1, "E241 multiple spaces after '%s'" % separator
-        found = line.find(separator + '\t')
-        if found > -1:
-            return found + 1, "E242 tab after '%s'" % separator
-
-
-def imports_on_separate_lines(logical_line):
-    """
-    Imports should usually be on separate lines.
-    """
-    line = logical_line
-    if line.startswith('import '):
-        found = line.find(',')
-        if found > -1:
-            return found, "E401 multiple imports on one line"
-
-
-def compound_statements(logical_line):
-    """
-    Compound statements (multiple statements on the same line) are
-    generally discouraged.
-    """
-    line = logical_line
-    found = line.find(':')
-    if -1 < found < len(line) - 1:
-        before = line[:found]
-        if (before.count('{') <= before.count('}') and # {'a': 1} (dict)
-            before.count('[') <= before.count(']') and # [1:2] (slice)
-            not re.search(r'\blambda\b', before)):     # lambda x: x
-            return found, "E701 multiple statements on one line (colon)"
-    found = line.find(';')
-    if -1 < found:
-        return found, "E702 multiple statements on one line (semicolon)"
-
-
-def python_3000_has_key(logical_line):
-    """
-    The {}.has_key() method will be removed in the future version of
-    Python. Use the 'in' operation instead, like:
-    d = {"a": 1, "b": 2}
-    if "b" in d:
-        print d["b"]
-    """
-    pos = logical_line.find('.has_key(')
-    if pos > -1:
-        return pos, "W601 .has_key() is deprecated, use 'in'"
-
-
-def python_3000_raise_comma(logical_line):
-    """
-    When raising an exception, use "raise ValueError('message')"
-    instead of the older form "raise ValueError, 'message'".
-
-    The paren-using form is preferred because when the exception arguments
-    are long or include string formatting, you don't need to use line
-    continuation characters thanks to the containing parentheses.  The older
-    form will be removed in Python 3000.
-    """
-    match = raise_comma_match(logical_line)
-    if match:
-        return match.start(1), "W602 deprecated form of raising exception"
-
-
-def dumb_equals_boolean(logical_line):
-    """
-    Using "if x == True:" or "if x == False:" is wrong in any case:
-
-    First if you already have a boolean, you don't need to compare it to
-    another boolean. Just use "if x:" or "if not x:".
-
-    Second, even if you have some sort of "tristate logic", not only using
-    True/False, but other values, then you want to use "if x is True:" or
-    "if x is False:" because there is exactly one True and one False object.
-    """
-    match = equals_boolean_search(logical_line)
-    if match:
-        return match.start(1), "E798 don't use 'x == <boolean>', but just 'x' or 'not x' or 'x is <boolean>'"
-
-
-def dumb_equals_None(logical_line):
-    """
-    Using "if x == None:" is wrong in any case:
-
-    You either want to use "if x is None:" (there is only 1 None object) or -
-    in some simple cases - just "if not x:".
-    """
-    match = equals_None_search(logical_line)
-    if match:
-        return match.start(1), "E799 don't use 'x == None', but just 'x is None' or 'not x'"
-
-
-##############################################################################
-# Helper functions
-##############################################################################
-
-
-def expand_indent(line):
-    """
-    Return the amount of indentation.
-    Tabs are expanded to the next multiple of 8.
-
-    >>> expand_indent('    ')
-    4
-    >>> expand_indent('\\t')
-    8
-    >>> expand_indent('    \\t')
-    8
-    >>> expand_indent('       \\t')
-    8
-    >>> expand_indent('        \\t')
-    16
-    """
-    result = 0
-    for char in line:
-        if char == '\t':
-            result = result / 8 * 8 + 8
-        elif char == ' ':
-            result += 1
-        else:
-            break
-    return result
-
-
-##############################################################################
-# Framework to run all checks
-##############################################################################
-
-
-def message(text):
-    """Print a message."""
-    # print >> sys.stderr, options.prog + ': ' + text
-    # print >> sys.stderr, text
-    print text
-
-
-def find_checks(argument_name):
-    """
-    Find all globally visible functions where the first argument name
-    starts with argument_name.
-    """
-    checks = []
-    function_type = type(find_checks)
-    for name, function in globals().iteritems():
-        if type(function) is function_type:
-            args = inspect.getargspec(function)[0]
-            if len(args) >= 1 and args[0].startswith(argument_name):
-                checks.append((name, function, args))
-    checks.sort()
-    return checks
-
-
-def mute_string(text):
-    """
-    Replace contents with 'xxx' to prevent syntax matching.
-
-    >>> mute_string('"abc"')
-    '"xxx"'
-    >>> mute_string("'''abc'''")
-    "'''xxx'''"
-    >>> mute_string("r'abc'")
-    "r'xxx'"
-    """
-    start = 1
-    end = len(text) - 1
-    # String modifiers (e.g. u or r)
-    if text.endswith('"'):
-        start += text.index('"')
-    elif text.endswith("'"):
-        start += text.index("'")
-    # Triple quotes
-    if text.endswith('"""') or text.endswith("'''"):
-        start += 2
-        end -= 2
-    return text[:start] + 'x' * (end - start) + text[end:]
-
-
-class Checker:
-    """
-    Load a Python source file, tokenize it, check coding style.
-    """
-
-    def __init__(self, filename):
-        self.filename = filename
-        self.lines = file(filename, 'rb').readlines()
-        self.physical_checks = find_checks('physical_line')
-        self.logical_checks = find_checks('logical_line')
-        options.counters['physical lines'] = \
-            options.counters.get('physical lines', 0) + len(self.lines)
-
-    def readline(self):
-        """
-        Get the next line from the input buffer.
-        """
-        self.line_number += 1
-        if self.line_number > len(self.lines):
-            return ''
-        return self.lines[self.line_number - 1]
-
-    def readline_check_physical(self):
-        """
-        Check and return the next physical line. This method can be
-        used to feed tokenize.generate_tokens.
-        """
-        line = self.readline()
-        if line:
-            self.check_physical(line)
-        return line
-
-    def run_check(self, check, argument_names):
-        """
-        Run a check plugin.
-        """
-        arguments = []
-        for name in argument_names:
-            arguments.append(getattr(self, name))
-        return check(*arguments)
-
-    def check_physical(self, line):
-        """
-        Run all physical checks on a raw input line.
-        """
-        self.physical_line = line
-        if self.indent_char is None and len(line) and line[0] in ' \t':
-            self.indent_char = line[0]
-        for name, check, argument_names in self.physical_checks:
-            result = self.run_check(check, argument_names)
-            if result is not None:
-                offset, text = result
-                self.report_error(self.line_number, offset, text, check)
-
-    def build_tokens_line(self):
-        """
-        Build a logical line from tokens.
-        """
-        self.mapping = []
-        logical = []
-        length = 0
-        previous = None
-        for token in self.tokens:
-            token_type, text = token[0:2]
-            if token_type in (tokenize.COMMENT, tokenize.NL,
-                              tokenize.INDENT, tokenize.DEDENT,
-                              tokenize.NEWLINE):
-                continue
-            if token_type == tokenize.STRING:
-                text = mute_string(text)
-            if previous:
-                end_line, end = previous[3]
-                start_line, start = token[2]
-                if end_line != start_line: # different row
-                    if self.lines[end_line - 1][end - 1] not in '{[(':
-                        logical.append(' ')
-                        length += 1
-                elif end != start: # different column
-                    fill = self.lines[end_line - 1][end:start]
-                    logical.append(fill)
-                    length += len(fill)
-            self.mapping.append((length, token))
-            logical.append(text)
-            length += len(text)
-            previous = token
-        self.logical_line = ''.join(logical)
-        assert self.logical_line.lstrip() == self.logical_line
-        assert self.logical_line.rstrip() == self.logical_line
-
-    def check_logical(self):
-        """
-        Build a line from tokens and run all logical checks on it.
-        """
-        options.counters['logical lines'] = \
-            options.counters.get('logical lines', 0) + 1
-        self.build_tokens_line()
-        first_line = self.lines[self.mapping[0][1][2][0] - 1]
-        indent = first_line[:self.mapping[0][1][2][1]]
-        self.previous_indent_level = self.indent_level
-        self.indent_level = expand_indent(indent)
-        if options.verbose >= 2:
-            print self.logical_line[:80].rstrip()
-        for name, check, argument_names in self.logical_checks:
-            if options.verbose >= 3:
-                print '   ', name
-            result = self.run_check(check, argument_names)
-            if result is not None:
-                offset, text = result
-                if type(offset) is tuple:
-                    original_number, original_offset = offset
-                else:
-                    for token_offset, token in self.mapping:
-                        if offset >= token_offset:
-                            original_number = token[2][0]
-                            original_offset = (token[2][1]
-                                               + offset - token_offset)
-                self.report_error(original_number, original_offset,
-                                  text, check)
-        self.previous_logical = self.logical_line
-
-    def check_all(self):
-        """
-        Run all checks on the input file.
-        """
-        self.file_errors = 0
-        self.line_number = 0
-        self.indent_char = None
-        self.indent_level = 0
-        self.previous_logical = ''
-        self.blank_lines = 0
-        self.tokens = []
-        parens = 0
-        for token in tokenize.generate_tokens(self.readline_check_physical):
-            # print tokenize.tok_name[token[0]], repr(token)
-            self.tokens.append(token)
-            token_type, text = token[0:2]
-            if token_type == tokenize.OP and text in '([{':
-                parens += 1
-            if token_type == tokenize.OP and text in '}])':
-                parens -= 1
-            if token_type == tokenize.NEWLINE and not parens:
-                self.check_logical()
-                self.blank_lines = 0
-                self.tokens = []
-            if token_type == tokenize.NL and not parens:
-                self.blank_lines += 1
-                self.tokens = []
-            if token_type == tokenize.COMMENT:
-                self.blank_lines = 0
-        return self.file_errors
-
-    def report_error(self, line_number, offset, text, check):
-        """
-        Report an error, according to options.
-        """
-        if options.quiet == 1 and not self.file_errors:
-            message(self.filename)
-        code = text[:4]
-        if ignore_code(code):
-            return
-        self.file_errors += 1
-        options.counters[code] = options.counters.get(code, 0) + 1
-        options.messages[code] = text[5:]
-        if options.quiet:
-            return
-        if options.testsuite:
-            base = os.path.basename(self.filename)[:4]
-            if base == code:
-                return
-            if base[0] == 'E' and code[0] == 'W':
-                return
-        if options.counters[code] == 1 or options.repeat:
-            message("%s:%s:%d: %s" %
-                    (self.filename, line_number, offset + 1, text))
-            if options.show_source:
-                line = self.lines[line_number - 1]
-                message(line.rstrip())
-                message(' ' * offset + '^')
-            if options.show_pep8:
-                message(check.__doc__.lstrip('\n').rstrip())
-
-
-def input_file(filename):
-    """
-    Run all checks on a Python source file.
-    """
-    if excluded(filename) or not filename_match(filename):
-        return {}
-    if options.verbose:
-        message('checking ' + filename)
-    options.counters['files'] = options.counters.get('files', 0) + 1
-    errors = Checker(filename).check_all()
-    if options.testsuite and not errors:
-        message("%s: %s" % (filename, "no errors found"))
-
-
-def input_dir(dirname):
-    """
-    Check all Python source files in this directory and all subdirectories.
-    """
-    dirname = dirname.rstrip('/')
-    if excluded(dirname):
-        return
-    for root, dirs, files in os.walk(dirname):
-        if options.verbose:
-            message('directory ' + root)
-        options.counters['directories'] = \
-            options.counters.get('directories', 0) + 1
-        dirs.sort()
-        for subdir in dirs:
-            if excluded(subdir):
-                dirs.remove(subdir)
-        files.sort()
-        for filename in files:
-            input_file(os.path.join(root, filename))
-
-
-def excluded(filename):
-    """
-    Check if options.exclude contains a pattern that matches filename.
-    """
-    basename = os.path.basename(filename)
-    for pattern in options.exclude:
-        if fnmatch(basename, pattern):
-            # print basename, 'excluded because it matches', pattern
-            return True
-
-
-def filename_match(filename):
-    """
-    Check if options.filename contains a pattern that matches filename.
-    If options.filename is unspecified, this always returns True.
-    """
-    if not options.filename:
-        return True
-    for pattern in options.filename:
-        if fnmatch(filename, pattern):
-            return True
-
-
-def ignore_code(code):
-    """
-    Check if options.ignore contains a prefix of the error code.
-    """
-    for ignore in options.ignore:
-        if code.startswith(ignore):
-            return True
-
-
-def get_error_statistics():
-    """Get error statistics."""
-    return get_statistics("E")
-
-
-def get_warning_statistics():
-    """Get warning statistics."""
-    return get_statistics("W")
-
-
-def get_statistics(prefix=''):
-    """
-    Get statistics for message codes that start with the prefix.
-
-    prefix='' matches all errors and warnings
-    prefix='E' matches all errors
-    prefix='W' matches all warnings
-    prefix='E4' matches all errors that have to do with imports
-    """
-    stats = []
-    keys = options.messages.keys()
-    keys.sort()
-    for key in keys:
-        if key.startswith(prefix):
-            stats.append('%-7s %s %s' %
-                         (options.counters[key], key, options.messages[key]))
-    return stats
-
-
-def print_statistics(prefix=''):
-    """Print overall statistics (number of errors and warnings)."""
-    for line in get_statistics(prefix):
-        print line
-
-
-def print_benchmark(elapsed):
-    """
-    Print benchmark numbers.
-    """
-    print '%-7.2f %s' % (elapsed, 'seconds elapsed')
-    keys = ['directories', 'files',
-            'logical lines', 'physical lines']
-    for key in keys:
-        if key in options.counters:
-            print '%-7d %s per second (%d total)' % (
-                options.counters[key] / elapsed, key,
-                options.counters[key])
-
-
-def process_options(arglist=None):
-    """
-    Process options passed either via arglist or via command line args.
-    """
-    global options, args
-    usage = "%prog [options] input ..."
-    parser = OptionParser(usage)
-    parser.add_option('-v', '--verbose', default=0, action='count',
-                      help="print status messages, or debug with -vv")
-    parser.add_option('-q', '--quiet', default=0, action='count',
-                      help="report only file names, or nothing with -qq")
-    parser.add_option('--exclude', metavar='patterns', default=default_exclude,
-                      help="skip matches (default %s)" % default_exclude)
-    parser.add_option('--filename', metavar='patterns',
-                      help="only check matching files (e.g. *.py)")
-    parser.add_option('--ignore', metavar='errors', default='',
-                      help="skip errors and warnings (e.g. E4,W)")
-    parser.add_option('--repeat', action='store_true',
-                      help="show all occurrences of the same error")
-    parser.add_option('--show-source', action='store_true',
-                      help="show source code for each error")
-    parser.add_option('--show-pep8', action='store_true',
-                      help="show text of PEP 8 for each error")
-    parser.add_option('--statistics', action='store_true',
-                      help="count errors and warnings")
-    parser.add_option('--benchmark', action='store_true',
-                      help="measure processing speed")
-    parser.add_option('--testsuite', metavar='dir',
-                      help="run regression tests from dir")
-    parser.add_option('--doctest', action='store_true',
-                      help="run doctest on myself")
-    options, args = parser.parse_args(arglist)
-    if options.testsuite:
-        args.append(options.testsuite)
-    if len(args) == 0:
-        parser.error('input not specified')
-    options.prog = os.path.basename(sys.argv[0])
-    options.exclude = options.exclude.split(',')
-    for index in range(len(options.exclude)):
-        options.exclude[index] = options.exclude[index].rstrip('/')
-    if options.filename:
-        options.filename = options.filename.split(',')
-    if options.ignore:
-        options.ignore = options.ignore.split(',')
-    else:
-        options.ignore = []
-    options.counters = {}
-    options.messages = {}
-
-    return options, args
-
-
-def _main():
-    """
-    Parse options and run checks on Python source.
-    """
-    options, args = process_options()
-    if options.doctest:
-        import doctest
-        return doctest.testmod()
-    start_time = time.time()
-    for path in args:
-        if os.path.isdir(path):
-            input_dir(path)
-        else:
-            input_file(path)
-    elapsed = time.time() - start_time
-    if options.statistics:
-        print_statistics()
-    if options.benchmark:
-        print_benchmark(elapsed)
-
-
-if __name__ == '__main__':
-    _main()
--- a/MoinMoin/_tests/test_error.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/_tests/test_error.py	Sat Oct 06 17:46:24 2012 +0200
@@ -73,4 +73,3 @@
         assert result == expected
 
 coverage_modules = ['MoinMoin.error']
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/_tests/test_forms.py	Sat Oct 06 17:46:24 2012 +0200
@@ -0,0 +1,43 @@
+# Copyright: 2012 MoinMoin:PavelSviderski
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+    MoinMoin - MoinMoin.forms Tests
+"""
+
+import datetime
+from calendar import timegm
+
+from MoinMoin.forms import DateTimeUNIX
+
+def test_datetimeunix():
+    dt = datetime.datetime(2012, 12, 21, 23, 45, 59)
+    timestamp = timegm(dt.timetuple())
+    dt_u = u'2012-12-21 23:45:59'
+
+    d = DateTimeUNIX(timestamp)
+    assert d.value == timestamp
+    assert d.u == dt_u
+
+    incorrect_timestamp = 999999999999
+    d = DateTimeUNIX(incorrect_timestamp)
+    assert d.value is None
+    assert d.u == unicode(incorrect_timestamp)
+
+    d = DateTimeUNIX(dt)
+    assert d.value == timestamp
+    assert d.u == dt_u
+
+    # parse time string
+    d = DateTimeUNIX(dt_u)
+    assert d.value == timestamp
+    assert d.u == dt_u
+
+    incorrect_timestring = u'2012-10-30'
+    d = DateTimeUNIX(incorrect_timestring)
+    assert d.value is None
+    assert d.u == incorrect_timestring
+
+    d = DateTimeUNIX(None)
+    assert d.value is None
+    assert d.u == u''
--- a/MoinMoin/_tests/test_sourcecode.py	Tue Mar 27 23:48:23 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,110 +0,0 @@
-# Copyright: 2006 by Armin Rigo (originally only testing for tab chars)
-# Copyright: 2007 adapted and extended (calling the PEP8 checker for most stuff) by MoinMoin:ThomasWaldmann.
-# License: MIT licensed
-
-"""
-Verify that the MoinMoin source files conform (mostly) to PEP8 coding style.
-
-Additionally, we check that the files have no crlf (Windows style) line endings.
-"""
-
-
-import re, time
-import pytest
-from . import pep8
-
-import py
-
-moindir = py.path.local(__file__).pypkgpath()
-
-EXCLUDE = set([
-    moindir/'static', # this is our dist static stuff
-    moindir/'_tests/wiki', # this is our test wiki
-])
-
-TRAILING_SPACES = 'nochange' # 'nochange' or 'fix'
-                             # use 'fix' with extreme caution and in a separate changeset!
-FIX_TS_RE = re.compile(r' +$', re.M) # 'fix' mode: everything matching the trailing space re will be removed
-
-try:
-    import xattr
-    if not hasattr(xattr, "xattr"): # there seem to be multiple modules with that name
-        raise ImportError
-    def mark_file_ok(path, mtime):
-        x = xattr.xattr(path)
-        try:
-            x.set('user.moin-pep8-tested-mtime', str(int(mtime)))
-        except IOError:
-            # probably not supported
-            global mark_file_ok
-            mark_file_ok = lambda path, mtime: None
-
-    def should_check_file(path, mtime):
-        x = xattr.xattr(path)
-        try:
-            mt = x.get('user.moin-pep8-tested-mtime')
-            mt = long(mt)
-            return mtime > mt
-        except IOError:
-            # probably not supported
-            global should_check_file
-            should_check_file = lambda path, mtime: True
-        return True
-except ImportError:
-    def mark_file_ok(path, mtime):
-        pass
-    def should_check_file(path, mtime):
-        return True
-
-RECENTLY = time.time() - 7 * 24*60*60 # we only check stuff touched recently.
-#RECENTLY = 0 # check everything!
-
-# After doing a fresh clone, this procedure is recommended:
-# 1. Run the tests once to see if everything is OK (as cloning updates the mtime,
-#    it will test every file).
-# 2. Before starting to make new changes, use "touch" to change all timestamps
-#    to a time before <RECENTLY>.
-# 3. Regularly run the tests, they will run much faster now.
-
-def pep8_error_count(path):
-    # process_options initializes some data structures and MUST be called before each Checker().check_all()
-    pep8.process_options(['pep8', '--ignore=E202,E221,E222,E241,E301,E302,E401,E501,E701,W391,W601,W602', '--show-source', 'dummy_path'])
-    error_count = pep8.Checker(path).check_all()
-    return error_count
-
-def check_py_file(reldir, path, mtime):
-    if TRAILING_SPACES == 'fix':
-        f = file(path, 'rb')
-        data = f.read()
-        f.close()
-        fixed = FIX_TS_RE.sub('', data)
-
-        # Don't write files if there's no need for that,
-        # as altering timestamps can be annoying with some tools.
-        if fixed == data:
-            return
-
-        f = file(path, 'wb')
-        f.write(fixed)
-        f.close()
-    # Please read and follow PEP8 - rerun this test until it does not fail any more,
-    # any type of error is only reported ONCE (even if there are multiple).
-    error_count = pep8_error_count(path)
-    assert error_count == 0
-    mark_file_ok(path, mtime)
-
-def pytest_generate_tests(metafunc):
-    for pyfile in sorted(moindir.visit('*.py', lambda p: p not in EXCLUDE)):
-        relpath = moindir.bestrelpath(pyfile)
-        metafunc.addcall(id=relpath, funcargs={'path': pyfile})
-
-def test_sourcecode(path):
-    mtime = path.stat().mtime
-    if mtime < RECENTLY:
-        pytest.skip("source change not recent")
-    if not should_check_file(str(path), mtime):
-        pytest.skip("source marked as should not be checked")
-
-    check_py_file(str(moindir.bestrelpath(path)), str(path), mtime)
-
-
--- a/MoinMoin/_tests/test_test_environ.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/_tests/test_test_environ.py	Sat Oct 06 17:46:24 2012 +0200
@@ -52,4 +52,3 @@
     def test_config(self):
         assert isinstance(app.cfg, wikiconfig.Config)
         assert app.cfg.content_acl == CONTENT_ACL
-
--- a/MoinMoin/_tests/test_user.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/_tests/test_user.py	Sat Oct 06 17:46:24 2012 +0200
@@ -342,6 +342,28 @@
         expected = [u'MoinTest:item_added']
         assert result == expected
 
+    # Sessions -------------------------------------------------------
+
+    def test_sessions(self):
+        name = u'Test_User_sessions'
+        password = name
+        self.createUser(name, password)
+        theUser = user.User(name=name, password=password)
+
+        # generate test token and validate it
+        test_token = theUser.generate_session_token()
+        result_success = theUser.validate_session(test_token)
+        assert result_success
+
+        # check if the token is saved
+        test_new_token = theUser.get_session_token()
+        assert test_token == test_new_token
+
+        # check if password change invalidates the token
+        theUser.set_password(password, False)
+        result_failure = theUser.validate_session(test_token)
+        assert not result_failure
+
     # Other ----------------------------------------------------------
 
     def test_recovery_token(self):
@@ -410,4 +432,3 @@
 
 
 coverage_modules = ['MoinMoin.user']
-
--- a/MoinMoin/_tests/test_wikiutil.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/_tests/test_wikiutil.py	Sat Oct 06 17:46:24 2012 +0200
@@ -31,12 +31,12 @@
 class TestAnchorNames(object):
     def test_anchor_name_encoding(self):
         tests = [
-            # text                    expected output
-            (u'\xf6\xf6ll\xdf\xdf',   'A.2BAPYA9g-ll.2BAN8A3w-'),
-            (u'level 2',              'level_2'),
-            (u'level_2',              'level_2'),
-            (u'',                     'A'),
-            (u'123',                  'A123'),
+            # text, expected output
+            (u'\xf6\xf6ll\xdf\xdf', 'A.2BAPYA9g-ll.2BAN8A3w-'),
+            (u'level 2', 'level_2'),
+            (u'level_2', 'level_2'),
+            (u'', 'A'),
+            (u'123', 'A123'),
             # make sure that a valid anchor is not modified:
             (u'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:_.-',
              u'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789:_.-')
@@ -224,12 +224,12 @@
 
 def testfile_headers():
     test_headers = [
-                #test_file               #content_type
-                ('imagefile.gif',       'image/gif'),
-                ('testfile.txt',        'text/plain'),
-                ('pdffile.pdf',         'application/pdf'),
-                ('docfile.doc',         'application/msword'),
-                (None,                  'application/octet-stream')
+                #test_file, content_type
+                ('imagefile.gif', 'image/gif'),
+                ('testfile.txt', 'text/plain'),
+                ('pdffile.pdf', 'application/pdf'),
+                ('docfile.doc', 'application/msword'),
+                (None, 'application/octet-stream')
                 ]
 
     for test_file, content_type in test_headers:
@@ -243,4 +243,3 @@
     assert result == expected
 
 coverage_modules = ['MoinMoin.wikiutil']
-
--- a/MoinMoin/_tests/wikiconfig.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/_tests/wikiconfig.py	Sat Oct 06 17:46:24 2012 +0200
@@ -19,10 +19,9 @@
     _here = abspath(dirname(__file__))
     _root = abspath(join(_here, '..', '..'))
     data_dir = join(_here, 'wiki', 'data') # needed for plugins package TODO
-    index_dir = join(_here, 'wiki', 'index')
+    index_storage = 'FileStorage', (join(_here, 'wiki', 'index'), ), {}
     content_acl = None
     item_root = 'FrontPage'
     interwikiname = u'MoinTest'
     interwiki_map = dict(Self='http://localhost:8080/', MoinMoin='http://moinmo.in/')
     interwiki_map[interwikiname] = 'http://localhost:8080/'
-
--- a/MoinMoin/app.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/app.py	Sat Oct 06 17:46:24 2012 +0200
@@ -20,8 +20,8 @@
 from flask import current_app as app
 from flask import g as flaskg
 
-from flaskext.cache import Cache
-from flaskext.themes import setup_themes
+from flask.ext.cache import Cache
+from flask.ext.themes import setup_themes
 
 from jinja2 import ChoiceLoader, FileSystemLoader
 
@@ -168,7 +168,7 @@
     if app.cfg.create_storage:
         app.router.create()
     app.router.open()
-    app.storage = indexing.IndexingMiddleware(app.cfg.index_dir, app.router,
+    app.storage = indexing.IndexingMiddleware(app.cfg.index_storage, app.router,
                                               wiki_name=app.cfg.interwikiname,
                                               acl_rights_contents=app.cfg.acl_rights_contents)
     if app.cfg.create_index:
@@ -218,6 +218,7 @@
         session['user.trusted'] = userobj.trusted
         session['user.auth_method'] = userobj.auth_method
         session['user.auth_attribs'] = userobj.auth_attribs
+        session['user.session_token'] = userobj.get_session_token()
     return userobj
 
 
@@ -260,4 +261,3 @@
         # can happen if teardown_wiki() is called twice, e.g. by unit tests.
         pass
     return response
-
--- a/MoinMoin/apps/admin/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/admin/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -11,4 +11,3 @@
 from flask import Blueprint
 admin = Blueprint('admin', __name__, template_folder='templates')
 import MoinMoin.apps.admin.views
-
--- a/MoinMoin/apps/admin/_tests/test_admin.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/admin/_tests/test_admin.py	Sat Oct 06 17:46:24 2012 +0200
@@ -13,10 +13,11 @@
             rv = c.get(url)
             assert rv.status == status
             assert rv.headers['Content-Type'] == 'text/html; charset=utf-8'
-            for item in data: assert item in rv.data
+            for item in data:
+                assert item in rv.data
 
     def test_index(self):
-        self._test_view_get(url_for('admin.index'))
+        self._test_view_get(url_for('admin.index'), status='403 FORBIDDEN')
 
     def test_userprofile(self):
         self._test_view_get(url_for('admin.userprofile', user_name='DoesntExist'), status='403 FORBIDDEN')
@@ -35,4 +36,3 @@
 
     def test_itemsize(self):
         self._test_view_get(url_for('admin.itemsize'))
-
--- a/MoinMoin/apps/admin/templates/admin/highlighterhelp.html	Tue Mar 27 23:48:23 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-{% import "utils.html" as utils %}
-{% extends theme("layout.html") %}
-{% block content %}
-<h1>{{ _("Available Highlighters") }}</h1>
-{{ utils.table(headings, rows) }}
-{% endblock %}
-
--- a/MoinMoin/apps/admin/templates/admin/index.html	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/admin/templates/admin/index.html	Sat Oct 06 17:46:24 2012 +0200
@@ -1,9 +1,5 @@
 {% extends theme("layout.html") %}
 {% block content %}
-<h1>{{ _("Documentation") }}</h1>
-<ul>
-    <li><a href="{{ url_for('serve.files', name='docs', filename='index.html') }}">{{ _("Documentation (local)") }}</a></li>
-</ul>
 <h1>{{ _("Admin Menu") }}</h1>
 <ul>
     <li><a href="{{ url_for('admin.userbrowser') }}">{{ _("User Browser") }}</a></li>
@@ -11,13 +7,4 @@
     <li><a href="{{ url_for('admin.wikiconfig') }}">{{ _("Show Wiki Configuration") }}</a></li>
     <li><a href="{{ url_for('admin.wikiconfighelp') }}">{{ _("Show Wiki Configuration Help") }}</a></li>
 </ul>
-<h1>{{ _("User Menu") }}</h1>
-<ul>
-    <li><a href="{{ url_for('frontend.wanted_items') }}">{{ _("Wanted Items") }}</a></li>
-    <li><a href="{{ url_for('frontend.orphaned_items') }}">{{ _("Orphaned Items") }}</a></li>
-    <li><a href="{{ url_for('admin.itemsize') }}">{{ _("Item sizes (latest revision)") }}</a></li>
-    <li><a href="{{ url_for('admin.interwikihelp') }}">{{ _("Known InterWiki names") }}</a></li>
-    <li><a href="{{ url_for('admin.highlighterhelp') }}">{{ _("Available Highlighters") }}</a></li>
-</ul>
 {% endblock %}
-
--- a/MoinMoin/apps/admin/templates/admin/interwikihelp.html	Tue Mar 27 23:48:23 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-{% import "utils.html" as utils %}
-{% extends theme("layout.html") %}
-{% block content %}
-<h1>{{ _("Known InterWiki names") }}</h1>
-{{ utils.table(headings, rows) }}
-{% endblock %}
-
--- a/MoinMoin/apps/admin/templates/admin/itemsize.html	Tue Mar 27 23:48:23 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,7 +0,0 @@
-{% import "utils.html" as utils %}
-{% extends theme("layout.html") %}
-{% block content %}
-<h1>{{ _("Item sizes (latest revision)") }}</h1>
-{{ utils.table(headings, rows) }}
-{% endblock %}
-
--- a/MoinMoin/apps/admin/templates/admin/sysitems_upgrade.html	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/admin/templates/admin/sysitems_upgrade.html	Sat Oct 06 17:46:24 2012 +0200
@@ -11,4 +11,3 @@
 </fieldset>
 </form>
 {% endblock %}
-
--- a/MoinMoin/apps/admin/templates/admin/userbrowser.html	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/admin/templates/admin/userbrowser.html	Sat Oct 06 17:46:24 2012 +0200
@@ -32,4 +32,3 @@
     {% endfor %}
 </table>
 {% endblock %}
-
--- a/MoinMoin/apps/admin/templates/admin/wikiconfig.html	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/admin/templates/admin/wikiconfig.html	Sat Oct 06 17:46:24 2012 +0200
@@ -33,4 +33,3 @@
 </tdbody>
 </table>
 {% endblock %}
-
--- a/MoinMoin/apps/admin/templates/admin/wikiconfighelp.html	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/admin/templates/admin/wikiconfighelp.html	Sat Oct 06 17:46:24 2012 +0200
@@ -26,4 +26,3 @@
     </table>
 {% endfor %}
 {% endblock %}
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/admin/templates/user/highlighterhelp.html	Sat Oct 06 17:46:24 2012 +0200
@@ -0,0 +1,6 @@
+{% import "utils.html" as utils %}
+{% extends theme("layout.html") %}
+{% block content %}
+<h1>{{ _("Available Highlighters") }}</h1>
+{{ utils.table(headings, rows) }}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/admin/templates/user/index_user.html	Sat Oct 06 17:46:24 2012 +0200
@@ -0,0 +1,15 @@
+{% extends theme("layout.html") %}
+{% block content %}
+<h1>{{ _("Documentation") }}</h1>
+<ul>
+    <li><a href="{{ url_for('serve.files', name='docs', filename='index.html') }}">{{ _("Documentation (local)") }}</a></li>
+</ul>
+<h1>{{ _("User Menu") }}</h1>
+<ul>
+    <li><a href="{{ url_for('frontend.wanted_items') }}">{{ _("Wanted Items") }}</a></li>
+    <li><a href="{{ url_for('frontend.orphaned_items') }}">{{ _("Orphaned Items") }}</a></li>
+    <li><a href="{{ url_for('admin.itemsize') }}">{{ _("Item sizes (latest revision)") }}</a></li>
+    <li><a href="{{ url_for('admin.interwikihelp') }}">{{ _("Known InterWiki names") }}</a></li>
+    <li><a href="{{ url_for('admin.highlighterhelp') }}">{{ _("Available Highlighters") }}</a></li>
+</ul>
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/admin/templates/user/interwikihelp.html	Sat Oct 06 17:46:24 2012 +0200
@@ -0,0 +1,6 @@
+{% import "utils.html" as utils %}
+{% extends theme("layout.html") %}
+{% block content %}
+<h1>{{ _("Known InterWiki names") }}</h1>
+{{ utils.table(headings, rows) }}
+{% endblock %}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/apps/admin/templates/user/itemsize.html	Sat Oct 06 17:46:24 2012 +0200
@@ -0,0 +1,6 @@
+{% import "utils.html" as utils %}
+{% extends theme("layout.html") %}
+{% block content %}
+<h1>{{ _("Item sizes (latest revision)") }}</h1>
+{{ utils.table(headings, rows) }}
+{% endblock %}
--- a/MoinMoin/apps/admin/views.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/admin/views.py	Sat Oct 06 17:46:24 2012 +0200
@@ -25,10 +25,15 @@
 from MoinMoin.config import SUPERUSER
 from MoinMoin.security import require_permission
 
-@admin.route('/')
+@admin.route('/superuser')
+@require_permission(SUPERUSER)
 def index():
     return render_template('admin/index.html', title_name=_(u"Admin"))
 
+@admin.route('/user')
+def index_user():
+    return render_template('user/index_user.html', title_name=_(u"User"))
+
 
 @admin.route('/userbrowser')
 @require_permission(SUPERUSER)
@@ -40,7 +45,7 @@
     revs = user.search_users() # all users
     user_accounts = [dict(uid=rev.meta[ITEMID],
                           name=rev.meta[NAME],
-                          email=u'', # rev.meta[EMAIL],  # TODO: fix KeyError
+                          email=rev.meta[EMAIL],
                           disabled=False,  # TODO: add to index
                           groups=[groupname for groupname in groups if rev.meta[NAME] in groups[groupname]],
                      )
@@ -77,9 +82,9 @@
         if ok:
             setattr(u, key, val)
             u.save()
-            flash('{0}.{1}: {2} -> {3}'.format(user_name, key, unicode(oldval), unicode(val), ), "info")
+            flash(u'{0}.{1}: {2} -> {3}'.format(user_name, key, unicode(oldval), unicode(val), ), "info")
         else:
-            flash('modifying {0}.{1} failed'.format(user_name, key, ), "error")
+            flash(u'modifying {0}.{1} failed'.format(user_name, key, ), "error")
     return redirect(url_for('.userbrowser'))
 
 
@@ -201,7 +206,7 @@
     lexers = pygments.lexers.get_all_lexers()
     rows = sorted([[desc, ' '.join(names), ' '.join(patterns), ' '.join(mimetypes), ]
                    for desc, names, patterns, mimetypes in lexers])
-    return render_template('admin/highlighterhelp.html',
+    return render_template('user/highlighterhelp.html',
                            title_name=_(u"Highlighter Help"),
                            headings=headings,
                            rows=rows)
@@ -214,7 +219,7 @@
                 _('URL'),
                ]
     rows = sorted(app.cfg.interwiki_map.items())
-    return render_template('admin/interwikihelp.html',
+    return render_template('user/interwikihelp.html',
                            title_name=_(u"Interwiki Help"),
                            headings=headings,
                            rows=rows)
@@ -229,9 +234,7 @@
     rows = [(rev.meta[SIZE], rev.name)
             for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname)]
     rows = sorted(rows, reverse=True)
-    return render_template('admin/itemsize.html',
+    return render_template('user/itemsize.html',
                            title_name=_(u"Item Size"),
                            headings=headings,
                            rows=rows)
-
-
--- a/MoinMoin/apps/feed/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/feed/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -12,4 +12,3 @@
 from flask import Blueprint
 feed = Blueprint('feed', __name__)
 import MoinMoin.apps.feed.views
-
--- a/MoinMoin/apps/feed/_tests/test_feed.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/feed/_tests/test_feed.py	Sat Oct 06 17:46:24 2012 +0200
@@ -44,4 +44,3 @@
             assert rv.headers['Content-Type'] == 'application/atom+xml'
             assert rv.data.startswith('<?xml')
             assert "checking if the cache invalidation works" in rv.data
-
--- a/MoinMoin/apps/feed/views.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/feed/views.py	Sat Oct 06 17:46:24 2012 +0200
@@ -75,10 +75,10 @@
                 if previous_revid is not None:
                     # HTML diff for subsequent revisions
                     previous_rev = item[previous_revid]
-                    content = hl_item._render_data_diff_atom(previous_rev, this_rev)
+                    content = hl_item.content._render_data_diff_atom(previous_rev, this_rev)
                 else:
                     # full html rendering for new items
-                    content = render_template('atom.html', get='first_revision', rev=this_rev, content=Markup(hl_item._render_data()), revision=this_revid)
+                    content = render_template('atom.html', get='first_revision', rev=this_rev, content=Markup(hl_item.content._render_data()), revision=this_revid)
                 content_type = 'html'
             except Exception as e:
                 logging.exception("content rendering crashed")
@@ -110,4 +110,3 @@
         if cid is not None:
             app.cache.set(cid, content)
     return Response(content, content_type='application/atom+xml')
-
--- a/MoinMoin/apps/frontend/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/frontend/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -12,4 +12,3 @@
 from flask import Blueprint
 frontend = Blueprint('frontend', __name__)
 import MoinMoin.apps.frontend.views
-
--- a/MoinMoin/apps/frontend/_tests/test_frontend.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/frontend/_tests/test_frontend.py	Sat Oct 06 17:46:24 2012 +0200
@@ -187,8 +187,11 @@
     def test_logout(self):
         self._test_view('frontend.logout', status='302 FOUND', data=['<!DOCTYPE HTML'])
 
-    def test_usersettings(self):
-        self._test_view('frontend.usersettings')
+    def test_usersettings_notloggedin(self):
+        # if a anon user visits usersettings view, he'll get redirected to the login view
+        self._test_view('frontend.usersettings', status='302 FOUND', data=['<!DOCTYPE HTML'])
+
+    # TODO: implement test_usersettings_loggedin()
 
     def test_bookmark(self):
         self._test_view('frontend.bookmark', status='302 FOUND', data=['<!DOCTYPE HTML'])
@@ -296,4 +299,3 @@
         if email is None:
             email = "user@example.org"
         user.create_user(name, password, email, is_encrypted=pwencoded)
-
--- a/MoinMoin/apps/frontend/views.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/frontend/views.py	Sat Oct 06 17:46:24 2012 +0200
@@ -1,3 +1,4 @@
+# Copyright: 2012 MoinMoin:CheerXiao
 # Copyright: 2003-2010 MoinMoin:ThomasWaldmann
 # Copyright: 2011 MoinMoin:AkashSinha
 # Copyright: 2011 MoinMoin:ReimarBauer
@@ -17,29 +18,28 @@
 import re
 import difflib
 import time
+import mimetypes
+import json
 from datetime import datetime
 from itertools import chain
 from collections import namedtuple
-try:
-    import json
-except ImportError:
-    import simplejson as json
+from functools import wraps, partial
 
-from flask import request, url_for, flash, Response, redirect, session, abort, jsonify
+from flask import request, url_for, flash, Response, make_response, redirect, session, abort, jsonify
 from flask import current_app as app
 from flask import g as flaskg
-from flaskext.babel import format_date
-from flaskext.themes import get_themes_list
+from flask.ext.babel import format_date
+from flask.ext.themes import get_themes_list
 
-from flatland import Form, String, Integer, Boolean, Enum, List
-from flatland.validation import Validator, Present, IsEmail, ValueBetween, URLValidator, Converted, ValueAtLeast
+from flatland import Form, Enum, List
+from flatland.validation import Validator
 
 from jinja2 import Markup
 
 import pytz
 from babel import Locale
 
-from whoosh.query import Term, And, Or, DateRange
+from whoosh.query import Term, Prefix, And, Or, DateRange, Every
 
 from MoinMoin import log
 logging = log.getLogger(__name__)
@@ -47,10 +47,10 @@
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.themes import render_template, get_editor_info, contenttype_to_class
 from MoinMoin.apps.frontend import frontend
-from MoinMoin.items import Item, NonExistent
-from MoinMoin.items import ROWS_META, COLS, ROWS_DATA
+from MoinMoin.forms import OptionalText, RequiredText, URL, YourOpenID, YourEmail, RequiredPassword, Checkbox, InlineCheckbox, Select, Tags, Natural, Submit, Hidden, MultiSelect
+from MoinMoin.items import BaseChangeForm, Item, NonExistent
+from MoinMoin.items.content import content_registry
 from MoinMoin import config, user, util
-from MoinMoin.config import CONTENTTYPE_GROUPS
 from MoinMoin.constants.keys import *
 from MoinMoin.util import crypto
 from MoinMoin.util.interwiki import url_for_item
@@ -124,9 +124,119 @@
     return app.send_static_file('logos/favicon.ico')
 
 
-@frontend.route('/+search', methods=['GET', 'POST'])
-def search():
-    title_name = _("Search")
+class LookupForm(Form):
+    name = OptionalText.using(label='name')
+    name_exact = OptionalText.using(label='name_exact')
+    itemid = OptionalText.using(label='itemid')
+    revid = OptionalText.using(label='revid')
+    userid = OptionalText.using(label='userid')
+    language = OptionalText.using(label='language')
+    itemlinks = OptionalText.using(label='itemlinks')
+    itemtransclusions = OptionalText.using(label='itemtransclusions')
+    refs = OptionalText.using(label='refs')
+    tags = Tags.using(optional=True).using(label='tags')
+    history = InlineCheckbox.using(label=L_('search also in non-current revisions'))
+    submit = Submit.using(default=L_('Lookup'))
+
+
+@frontend.route('/+lookup', methods=['GET', 'POST'])
+def lookup():
+    """
+    lookup is like search, but it only deals with specific fields that identify
+    an item / revision. no query string parsing.
+
+    for uuid fields, it performs a prefix search, so you can just give the
+    first few digits. same is done for name_exact field.
+    if you give a complete uuid or you do a lookup via the name field, it
+    will use a simple search term.
+    for one result, it directly redirects to the item/revision found.
+    for none or multipe results, a result page is shown.
+
+    usually this is used for links with a query string, like:
+    /+lookup?itemid=123cba  (prefix match on itemid 123cba.......)
+    /+lookup?revid=c0ddcda9a092499c92920cc4a9b11704  (full uuid simple term match)
+    /+lookup?name_exact=FooBar/  (prefix match on name_exact FooBar/...)
+
+    When giving history=1 it will use the all revisions index for lookup.
+    """
+    status = 200
+    title_name = _("Lookup")
+    # TAGS might be there multiple times, thus we need multi:
+    lookup_form = LookupForm.from_flat(request.values.items(multi=True))
+    valid = lookup_form.validate()
+    lookup_form['submit'].set_default() # XXX from_flat() kills all values
+    if valid:
+        history = bool(request.values.get('history'))
+        idx_name = ALL_REVS if history else LATEST_REVS
+        terms = []
+        for key in [NAME, NAME_EXACT, ITEMID, REVID, USERID,
+                    LANGUAGE,
+                    TAGS,
+                    ITEMLINKS, ITEMTRANSCLUSIONS, 'refs', ]:
+            value = lookup_form[key].value
+            if value:
+                if (key in [ITEMID, REVID, USERID, ] and len(value) < crypto.UUID_LEN
+                    or
+                    key in [NAME_EXACT]):
+                    term = Prefix(key, value)
+                elif key == 'refs':
+                    term = Or([Term(ITEMLINKS, value), Term(ITEMTRANSCLUSIONS, value)])
+                elif key == TAGS:
+                    term = And([Term(TAGS, v.value) for v in lookup_form[key]])
+                else:
+                    term = Term(key, value)
+                terms.append(term)
+        if terms:
+            terms.append(Term(WIKINAME, app.cfg.interwikiname))
+            q = And(terms)
+            with flaskg.storage.indexer.ix[idx_name].searcher() as searcher:
+                flaskg.clock.start('lookup')
+                results = searcher.search(q, limit=100)
+                flaskg.clock.stop('lookup')
+                num_results = results.scored_length()
+                if num_results == 1:
+                    result = results[0]
+                    rev = result[REVID] if history else CURRENT
+                    url = url_for('.show_item', item_name=result[NAME], rev=rev)
+                    return redirect(url)
+                else:
+                    flaskg.clock.start('lookup render')
+                    html = render_template('lookup.html',
+                                           title_name=title_name,
+                                           lookup_form=lookup_form,
+                                           results=results,
+                                          )
+                    flaskg.clock.stop('lookup render')
+                    if not num_results:
+                        status = 404
+                    return Response(html, status)
+    html = render_template('lookup.html',
+                           title_name=title_name,
+                           lookup_form=lookup_form,
+                          )
+    return Response(html, status)
+
+
+def _compute_item_transclusions(item_name):
+    """Compute which items are transcluded into item <item_name>.
+
+    :returns: a set of their item names.
+    """
+    with flaskg.storage.indexer.ix[LATEST_REVS].searcher() as searcher:
+        # The search process should be as fast as possible so use
+        # the indexer low-level documents instead of high-level Revisions.
+        doc = searcher.document(name_exact=item_name)
+        if not doc:
+            return set()
+        transcluded_names = set(doc[ITEMTRANSCLUSIONS])
+        for item_name in transcluded_names.copy():
+            transclusions = _compute_item_transclusions(item_name)
+            transcluded_names.update(transclusions)
+        return transcluded_names
+
+@frontend.route('/+search/<itemname:item_name>', methods=['GET', 'POST'])
+@frontend.route('/+search', defaults=dict(item_name=u''), methods=['GET', 'POST'])
+def search(item_name):
     search_form = SearchForm.from_flat(request.values)
     valid = search_form.validate()
     search_form['submit'].set_default() # XXX from_flat() kills all values
@@ -134,11 +244,40 @@
     if valid:
         history = bool(request.values.get('history'))
         idx_name = ALL_REVS if history else LATEST_REVS
-        qp = flaskg.storage.query_parser([NAME_EXACT, NAME, CONTENT], idx_name=idx_name)
+        qp = flaskg.storage.query_parser([NAME_EXACT, NAME, SUMMARY, CONTENT], idx_name=idx_name)
         q = qp.parse(query)
+
+        _filter = None
+        if item_name: # Only search this item and subitems
+            prefix_name = item_name + u'/'
+            terms = [Term(NAME_EXACT, item_name), Prefix(NAME_EXACT, prefix_name), ]
+
+            show_transclusions = True
+            if show_transclusions:
+                # XXX Search subitems and all transcluded items (even recursively),
+                # still looks like a hack. Imaging you have "foo" on main page and
+                # "bar" on transcluded one. Then you search for "foo AND bar".
+                # Such stuff would only work if we expand transcluded items
+                # at indexing time (and we currently don't).
+                with flaskg.storage.indexer.ix[LATEST_REVS].searcher() as searcher:
+                    subq = Or([Term(NAME_EXACT, item_name), Prefix(NAME_EXACT, prefix_name), ])
+                    subq = And([subq, Every(ITEMTRANSCLUSIONS), ])
+                    flaskg.clock.start('search subitems with transclusions')
+                    results = searcher.search(subq, limit=None)
+                    flaskg.clock.stop('search subitems with transclusions')
+                    transcluded_names = set()
+                    for hit in results:
+                        name = hit[NAME]
+                        transclusions = _compute_item_transclusions(name)
+                        transcluded_names.update(transclusions)
+                # XXX Will whoosh cope with such a large filter query?
+                terms.extend([Term(NAME_EXACT, name) for name in transcluded_names])
+
+            _filter = Or(terms)
+
         with flaskg.storage.indexer.ix[idx_name].searcher() as searcher:
             flaskg.clock.start('search')
-            results = searcher.search(q, limit=100)
+            results = searcher.search(q, filter=_filter, limit=100)
             flaskg.clock.stop('search')
             # XXX if found that calling key_terms like you see below is 1000..10000x
             # slower than the search itself, so we better don't do that right now.
@@ -158,19 +297,56 @@
                                    content_suggestions=content_suggestions,
                                    query=query,
                                    medium_search_form=search_form,
-                                   title_name=title_name,
+                                   item_name=item_name,
                                   )
             flaskg.clock.stop('search render')
     else:
         html = render_template('search.html',
                                query=query,
                                medium_search_form=search_form,
-                               title_name=title_name,
+                               item_name=item_name,
                               )
     return html
 
 
-@frontend.route('/<itemname:item_name>', defaults=dict(rev=CURRENT), methods=['GET'])
+def add_presenter(wrapped, view, add_trail=False, abort404=True):
+    """
+    Add new "presenter" views.
+
+    Presenter views handle GET requests to locations like
+    +{view}/+<rev>/<item_name> and +{view}/<item_name>, and always try to
+    look up the item before processing.
+
+    :param view: name of view
+    :param add_trail: whether to call flaskg.user.add_trail
+    :param abort404: whether to abort(404) for nonexistent items
+    """
+    @frontend.route('/+{view}/+<rev>/<itemname:item_name>'.format(view=view))
+    @frontend.route('/+{view}/<itemname:item_name>'.format(view=view), defaults=dict(rev=CURRENT))
+    @wraps(wrapped)
+    def wrapper(item_name, rev):
+        if add_trail:
+            flaskg.user.add_trail(item_name)
+        try:
+            item = Item.create(item_name, rev_id=rev)
+        except AccessDenied:
+            abort(403)
+        if abort404 and isinstance(item, NonExistent):
+            abort(404, item_name)
+        return wrapped(item)
+    return wrapper
+
+
+def presenter(view, add_trail=False, abort404=True):
+    """
+    Decorator factory to apply add_presenter().
+    """
+    return partial(add_presenter, view=view, add_trail=add_trail, abort404=abort404)
+
+
+# The first form accepts POST to allow modifying behavior like modify_item.
+# The second form only accpets GET since modifying a historical revision is not allowed (yet).
+@frontend.route('/<itemname:item_name>', defaults=dict(rev=CURRENT), methods=['GET', 'POST'])
 @frontend.route('/+show/+<rev>/<itemname:item_name>', methods=['GET'])
 def show_item(item_name, rev):
     flaskg.user.add_trail(item_name)
@@ -180,24 +356,7 @@
         item = Item.create(item_name, rev_id=rev)
     except AccessDenied:
         abort(403)
-    show_revision = rev != CURRENT
-    show_navigation = False # TODO
-    first_rev = last_rev = None # TODO
-    if isinstance(item, NonExistent):
-        status = 404
-    else:
-        status = 200
-    content = render_template('show.html',
-                              item=item, item_name=item.name,
-                              rev=item.rev,
-                              contenttype=item.contenttype,
-                              first_rev_id=first_rev,
-                              last_rev_id=last_rev,
-                              data_rendered=Markup(item._render_data()),
-                              show_revision=show_revision,
-                              show_navigation=show_navigation,
-                             )
-    return Response(content, status)
+    return item.do_show(rev)
 
 
 @frontend.route('/+show/<itemname:item_name>')
@@ -205,19 +364,14 @@
     return redirect(url_for_item(item_name))
 
 
-@frontend.route('/+dom/+<rev>/<itemname:item_name>')
-@frontend.route('/+dom/<itemname:item_name>', defaults=dict(rev=CURRENT))
-def show_dom(item_name, rev):
-    try:
-        item = Item.create(item_name, rev_id=rev)
-    except AccessDenied:
-        abort(403)
+@presenter('dom', abort404=False)
+def show_dom(item):
     if isinstance(item, NonExistent):
         status = 404
     else:
         status = 200
     content = render_template('dom.xml',
-                              data_xml=Markup(item._render_data_xml()),
+                              data_xml=Markup(item.content._render_data_xml()),
                              )
     return Response(content, status, mimetype='text/xml')
 
@@ -236,32 +390,17 @@
     return Response(content, 200, mimetype='text/plain')
 
 
-@frontend.route('/+highlight/+<rev>/<itemname:item_name>')
-@frontend.route('/+highlight/<itemname:item_name>', defaults=dict(rev=CURRENT))
-def highlight_item(item_name, rev):
-    try:
-        item = Item.create(item_name, rev_id=rev)
-    except AccessDenied:
-        abort(403)
-    if isinstance(item, NonExistent):
-        abort(404, item_name)
+@presenter('highlight')
+def highlight_item(item):
     return render_template('highlight.html',
                            item=item, item_name=item.name,
-                           data_text=Markup(item._render_data_highlight()),
+                           data_text=Markup(item.content._render_data_highlight()),
                           )
 
 
-@frontend.route('/+meta/<itemname:item_name>', defaults=dict(rev=CURRENT))
-@frontend.route('/+meta/+<rev>/<itemname:item_name>')
-def show_item_meta(item_name, rev):
-    flaskg.user.add_trail(item_name)
-    try:
-        item = Item.create(item_name, rev_id=rev)
-    except AccessDenied:
-        abort(403)
-    if isinstance(item, NonExistent):
-        abort(404, item_name)
-    show_revision = rev != CURRENT
+@presenter('meta', add_trail=True)
+def show_item_meta(item):
+    show_revision = request.view_args['rev'] != CURRENT
     show_navigation = False # TODO
     first_rev = None
     last_rev = None
@@ -300,27 +439,17 @@
         abort(404, item_name)
     return render_template('content.html',
                            item_name=item.name,
-                           data_rendered=Markup(item._render_data()),
+                           data_rendered=Markup(item.content._render_data()),
                            )
 
-@frontend.route('/+get/+<rev>/<itemname:item_name>')
-@frontend.route('/+get/<itemname:item_name>', defaults=dict(rev=CURRENT))
-def get_item(item_name, rev):
-    try:
-        item = Item.create(item_name, rev_id=rev)
-    except AccessDenied:
-        abort(403)
-    return item.do_get()
+@presenter('get')
+def get_item(item):
+    return item.content.do_get()
 
-@frontend.route('/+download/+<rev>/<itemname:item_name>')
-@frontend.route('/+download/<itemname:item_name>', defaults=dict(rev=CURRENT))
-def download_item(item_name, rev):
-    try:
-        item = Item.create(item_name, rev_id=rev)
-        mimetype = request.values.get("mimetype")
-    except AccessDenied:
-        abort(403)
-    return item.do_get(force_attachment=True, mimetype=mimetype)
+@presenter('download')
+def download_item(item):
+    mimetype = request.values.get("mimetype")
+    return item.content.do_get(force_attachment=True, mimetype=mimetype)
 
 @frontend.route('/+convert/<itemname:item_name>')
 def convert_item(item_name):
@@ -343,10 +472,11 @@
     # XXX Maybe use a random name to be sure it does not exist
     item_name_converted = item_name + 'converted'
     try:
-        converted_item = Item.create(item_name_converted, contenttype=contenttype)
+        # TODO implement Content.create and use it here
+        converted_item = Item.create(item_name_converted, itemtype=u'default', contenttype=contenttype)
     except AccessDenied:
         abort(403)
-    return converted_item._convert(item.internal_representation())
+    return converted_item.content._convert(item.content.internal_representation())
 
 
 @frontend.route('/+modify/<itemname:item_name>', methods=['GET', 'POST'])
@@ -357,47 +487,33 @@
     On POST, saves the new page (unless there's an error in input).
     After successful POST, redirects to the page.
     """
+    # XXX drawing applets don't send itemtype
+    itemtype = request.values.get('itemtype', u'default')
     contenttype = request.values.get('contenttype')
-    template_name = request.values.get('template')
     try:
-        item = Item.create(item_name, contenttype=contenttype)
+        item = Item.create(item_name, itemtype=itemtype, contenttype=contenttype)
     except AccessDenied:
         abort(403)
     if not flaskg.user.may.write(item_name):
         abort(403)
-    return item.do_modify(contenttype, template_name)
+    return item.do_modify()
 
 
-class CommentForm(TextChaizedForm):
-    comment = String.using(label=L_('Comment'), optional=True).with_properties(placeholder=L_("Comment about your change"))
-    submit = String.using(default=L_('OK'), optional=True)
+class TargetChangeForm(BaseChangeForm):
+    target = RequiredText.using(label=L_('Target')).with_properties(placeholder=L_("The name of the target item"))
 
-class TargetCommentForm(CommentForm):
-    target = String.using(label=L_('Target')).with_properties(placeholder=L_("The name of the target item")).validated_by(Present())
-
-class RevertItemForm(CommentForm):
+class RevertItemForm(BaseChangeForm):
     name = 'revert_item'
 
-class DeleteItemForm(CommentForm):
+class DeleteItemForm(BaseChangeForm):
     name = 'delete_item'
 
-class DestroyItemForm(CommentForm):
+class DestroyItemForm(BaseChangeForm):
     name = 'destroy_item'
 
-class RenameItemForm(TargetCommentForm):
+class RenameItemForm(TargetChangeForm):
     name = 'rename_item'
 
-class ContenttypeFilterForm(Form):
-    name = 'contenttype_filter'
-    markup_text_items = Boolean.using(label=L_('markup text'), optional=True, default=1)
-    other_text_items = Boolean.using(label=L_('other text'), optional=True, default=1)
-    image_items = Boolean.using(label=L_('image'), optional=True, default=1)
-    audio_items = Boolean.using(label=L_('audio'), optional=True, default=1)
-    video_items = Boolean.using(label=L_('video'), optional=True, default=1)
-    other_items = Boolean.using(label=L_('other'), optional=True, default=1)
-    unknown_items = Boolean.using(label=L_('unknown'), optional=True, default=1)
-    submit = String.using(default=L_('Filter'), optional=True)
-
 
 @frontend.route('/+revert/+<rev>/<itemname:item_name>', methods=['GET', 'POST'])
 def revert_item(item_name, rev):
@@ -416,7 +532,7 @@
         form = RevertItemForm.from_flat(request.form)
         TextCha(form).amend_form()
         if form.validate():
-            item.revert()
+            item.revert(form['comment'])
             return redirect(url_for_item(item_name))
     return render_template(item.revert_template,
                            item=item, item_name=item_name,
@@ -590,6 +706,7 @@
     data_file = request.files.get('data_file')
     subitem_name = data_file.filename
     contenttype = data_file.content_type # guess by browser, based on file name
+    data = data_file.stream
     if item_name:
         subitem_prefix = item_name + u'/'
     else:
@@ -597,7 +714,7 @@
     item_name = subitem_prefix + subitem_name
     try:
         item = Item.create(item_name)
-        revid, size = item.modify()
+        revid, size = item.modify({}, data, contenttype_guessed=contenttype)
         item_modified.send(app._get_current_object(),
                            item_name=item_name)
         return jsonify(name=subitem_name,
@@ -609,6 +726,18 @@
         abort(403)
 
 
+contenttype_groups = content_registry.group_names[:]
+contenttype_group_descriptions = {}
+for g in contenttype_groups:
+    contenttype_group_descriptions[g] = ', '.join([e.display_name for e in content_registry.groups[g]])
+contenttype_groups.append('unknown items')
+
+ContenttypeGroup = MultiSelect.of(Enum.using(valid_values=contenttype_groups).with_properties(descriptions=contenttype_group_descriptions)).using(optional=True)
+
+class IndexForm(Form):
+    contenttype = ContenttypeGroup
+    submit = Submit.using(default=L_('Filter'))
+
 @frontend.route('/+index/', defaults=dict(item_name=''), methods=['GET', 'POST'])
 @frontend.route('/+index/<itemname:item_name>', methods=['GET', 'POST'])
 def index(item_name):
@@ -617,45 +746,36 @@
     except AccessDenied:
         abort(403)
 
-    if request.method == 'GET':
-        form = ContenttypeFilterForm.from_defaults()
-        selected_groups = None
-    elif request.method == "POST":
-        form = ContenttypeFilterForm.from_flat(request.form)
-        selected_groups = [gname.replace("_", " ") for gname, value in form.iteritems()
-                           if form[gname].value]
-        if u'submit' in selected_groups:
-            selected_groups.remove(u'submit')
-        if not selected_groups:
-            form = ContenttypeFilterForm.from_defaults()
+    # request.args is a MultiDict instance, which degenerates into a normal
+    # single-valued dict on most occasions (making the first value the *only*
+    # value for a specific key) unless explicitly told to expose multiple
+    # values, eg. calling items with multi=True. See Werkzeug documentation for
+    # more.
+    form = IndexForm.from_flat(request.args.items(multi=True))
+    form['submit'].set_default() # XXX from_flat() kills all values
+    if not form['contenttype']:
+        form['contenttype'].set(contenttype_groups)
 
+    selected_groups = form['contenttype'].value
     startswith = request.values.get("startswith")
-    index = item.flat_index(startswith, selected_groups)
 
-    ct_groups = [(gname, ", ".join([ctlabel for ctname, ctlabel in contenttypes]))
-                 for gname, contenttypes in CONTENTTYPE_GROUPS]
-    ct_groups = dict(ct_groups)
-
-    initials = item.name_initial(item.flat_index())
+    initials = item.name_initial(item.get_subitem_revs())
     initials = [initial.upper() for initial in initials]
     initials = list(set(initials))
     initials = sorted(initials)
-    detailed_index = item.get_detailed_index(index)
-    detailed_index = sorted(detailed_index, key=lambda name: name[0].lower())
+
+    dirs, files = item.get_index(startswith, selected_groups)
+    # index = sorted(index, key=lambda e: e.relname.lower())
 
     item_names = item_name.split(u'/')
-    if item_name:
-        args = dict(item_name=item_name)
-    else:
-        args = dict(item_name=u'', title_name=_(u'Global Index'))
     return render_template(item.index_template,
                            item_names=item_names,
-                           index=detailed_index,
+                           item_name=item_name,
+                           files=files,
+                           dirs=dirs,
                            initials=initials,
                            startswith=startswith,
-                           contenttype_groups=ct_groups,
                            form=form,
-                           **args
                           )
 
 
@@ -889,31 +1009,23 @@
     """a simple user registration form"""
     name = 'register'
 
-    username = String.using(label=L_('Name')).with_properties(placeholder=L_("The login name you want to use")).validated_by(Present())
-    password1 = String.using(label=L_('Password')).with_properties(placeholder=L_("The login password you want to use")).validated_by(Present())
-    password2 = String.using(label=L_('Password')).with_properties(placeholder=L_("Repeat the same password")).validated_by(Present())
-    email = String.using(label=L_('E-Mail')).with_properties(placeholder=L_("Your E-Mail address")).validated_by(IsEmail())
-    openid = String.using(label=L_('OpenID'), optional=True).with_properties(placeholder=L_("Your OpenID address")).validated_by(URLValidator())
-    submit = String.using(default=L_('Register'), optional=True)
+    username = RequiredText.using(label=L_('Name')).with_properties(placeholder=L_("The login name you want to use"))
+    password1 = RequiredPassword.with_properties(placeholder=L_("The login password you want to use"))
+    password2 = RequiredPassword.with_properties(placeholder=L_("Repeat the same password"))
+    email = YourEmail
+    openid = YourOpenID.using(optional=True)
+    submit = Submit.using(default=L_('Register'))
 
     validators = [ValidRegistration()]
 
 
-class OpenIDForm(TextChaizedForm):
+class OpenIDForm(RegistrationForm):
     """
     OpenID registration form, inherited from the simple registration form.
     """
     name = 'openid'
 
-    username = String.using(label=L_('Name')).with_properties(placeholder=L_("The login name you want to use")).validated_by(Present())
-    password1 = String.using(label=L_('Password')).with_properties(placeholder=L_("The login password you want to use")).validated_by(Present())
-    password2 = String.using(label=L_('Password')).with_properties(placeholder=L_("Repeat the same password")).validated_by(Present())
-
-    email = String.using(label=L_('E-Mail')).with_properties(placeholder=L_("Your E-Mail address")).validated_by(IsEmail())
-    openid = String.using(label=L_('OpenID')).with_properties(placeholder=L_("Your OpenID address")).validated_by(URLValidator())
-    submit = String.using(optional=True)
-
-    validators = [ValidRegistration()]
+    openid = YourOpenID
 
 def _using_moin_auth():
     """Check if MoinAuth is being used for authentication.
@@ -1032,9 +1144,9 @@
     """a simple password lost form"""
     name = 'lostpass'
 
-    username = String.using(label=L_('Name'), optional=True).with_properties(placeholder=L_("Your login name"))
-    email = String.using(label=L_('E-Mail'), optional=True).with_properties(placeholder=L_("Your E-Mail address")).validated_by(IsEmail())
-    submit = String.using(default=L_('Recover password'), optional=True)
+    username = OptionalText.using(label=L_('Name')).with_properties(placeholder=L_("Your login name"))
+    email = YourEmail.using(optional=True)
+    submit = Submit.using(default=L_('Recover password'))
 
     validators = [ValidLostPassword()]
 
@@ -1059,7 +1171,7 @@
             email = form['email'].value
             if form['email'].valid and email:
                 users = user.search_users(email=email)
-                u = users and user.User(users[0][ITEMID])
+                u = users and user.User(users[0].meta[ITEMID])
             if u and u.valid:
                 is_ok, msg = u.mail_password_recovery()
                 if not is_ok:
@@ -1092,11 +1204,11 @@
     """a simple password recovery form"""
     name = 'recoverpass'
 
-    username = String.using(label=L_('Name')).with_properties(placeholder=L_("Your login name")).validated_by(Present())
-    token = String.using(label=L_('Recovery token')).with_properties(placeholder=L_("The recovery token that has been sent to you")).validated_by(Present())
-    password1 = String.using(label=L_('New password')).with_properties(placeholder=L_("The login password you want to use")).validated_by(Present())
-    password2 = String.using(label=L_('New password (repeat)')).with_properties(placeholder=L_("Repeat the same password")).validated_by(Present())
-    submit = String.using(default=L_('Change password'), optional=True)
+    username = RequiredText.using(label=L_('Name')).with_properties(placeholder=L_("Your login name"))
+    token = RequiredText.using(label=L_('Recovery token')).with_properties(placeholder=L_("The recovery token that has been sent to you"))
+    password1 = RequiredPassword.using(label=L_('New password')).with_properties(placeholder=L_("The login password you want to use"))
+    password2 = RequiredPassword.using(label=L_('New password (repeat)')).with_properties(placeholder=L_("Repeat the same password"))
+    submit = Submit.using(default=L_('Change password'))
 
     validators = [ValidPasswordRecovery()]
 
@@ -1159,12 +1271,10 @@
     """
     name = 'login'
 
-    username = String.using(label=L_('Name'), optional=False).with_properties(autofocus=True).validated_by(Present())
-    password = String.using(label=L_('Password'), optional=False).validated_by(Present())
-    openid = String.using(label=L_('OpenID'), optional=True).validated_by(Present(), URLValidator())
-
-    # the submit hidden field
-    submit = String.using(optional=True)
+    username = RequiredText.using(label=L_('Name'), optional=False).with_properties(autofocus=True)
+    password = RequiredPassword
+    openid = YourOpenID.using(optional=True)
+    submit = Submit.using(default=L_('Log in'))
 
     validators = [ValidLogin()]
 
@@ -1199,16 +1309,10 @@
                           )
 
 
-def _logout():
-    for key in ['user.itemid', 'user.trusted', 'user.auth_method', 'user.auth_attribs', ]:
-        if key in session:
-            del session[key]
-
-
 @frontend.route('/+logout')
 def logout():
     flash(_("You are now logged out."), "info")
-    _logout()
+    flaskg.user.logout_session()
     return redirect(url_for('.show_root'))
 
 
@@ -1238,23 +1342,23 @@
 
 class UserSettingsPasswordForm(Form):
     name = 'usersettings_password'
-    password_current = String.using(label=L_('Current Password')).with_properties(placeholder=L_("Your current login password")).validated_by(Present())
-    password1 = String.using(label=L_('New password')).with_properties(placeholder=L_("The login password you want to use")).validated_by(Present())
-    password2 = String.using(label=L_('New password (repeat)')).with_properties(placeholder=L_("Repeat the same password")).validated_by(Present())
-    submit = String.using(default=L_('Change password'), optional=True)
     validators = [ValidChangePass()]
 
+    password_current = RequiredPassword.using(label=L_('Current Password')).with_properties(placeholder=L_("Your current login password"))
+    password1 = RequiredPassword.using(label=L_('New password')).with_properties(placeholder=L_("The login password you want to use"))
+    password2 = RequiredPassword.using(label=L_('New password (repeat)')).with_properties(placeholder=L_("Repeat the same password"))
+    submit = Submit.using(default=L_('Change password'))
 
 class UserSettingsNotificationForm(Form):
     name = 'usersettings_notification'
-    email = String.using(label=L_('E-Mail')).with_properties(placeholder=L_("Your E-Mail address")).validated_by(IsEmail())
-    submit = String.using(default=L_('Save'), optional=True)
+    email = YourEmail
+    submit = Submit.using(default=L_('Save'))
 
 
 class UserSettingsNavigationForm(Form):
     name = 'usersettings_navigation'
     # TODO: find a good way to handle quicklinks here
-    submit = String.using(default=L_('Save'), optional=True)
+    submit = Submit.using(default=L_('Save'))
 
 
 class UserSettingsOptionsForm(Form):
@@ -1265,11 +1369,12 @@
     # builtin defaults (for some True, for some others False). Makes
     # edit_on_doubleclick malfunctioning (because its default is True).
     name = 'usersettings_options'
-    mailto_author = Boolean.using(label=L_('Publish my email (not my wiki homepage) in author info'), optional=True)
-    edit_on_doubleclick = Boolean.using(label=L_('Open editor on double click'), optional=True)
-    show_comments = Boolean.using(label=L_('Show comment sections'), optional=True)
-    disabled = Boolean.using(label=L_('Disable this account forever'), optional=True)
-    submit = String.using(default=L_('Save'), optional=True)
+    mailto_author = Checkbox.using(label=L_('Publish my email (not my wiki homepage) in author info'))
+    edit_on_doubleclick = Checkbox.using(label=L_('Open editor on double click'))
+    scroll_page_after_edit = Checkbox.using(label=L_('Scroll page after edit'))
+    show_comments = Checkbox.using(label=L_('Show comment sections'))
+    disabled = Checkbox.using(label=L_('Disable this account forever'))
+    submit = Submit.using(default=L_('Save'))
 
 
 @frontend.route('/+usersettings', methods=['GET', 'POST'])
@@ -1280,29 +1385,29 @@
     # these forms can't be global because we need app object, which is only available within a request:
     class UserSettingsPersonalForm(Form):
         name = 'usersettings_personal' # "name" is duplicate
-        name = List.using(label=L_('Name')).of(String.with_properties(placeholder=L_("The login name you want to use")).validated_by(Present()))
-        display_name = String.using(label=L_('Display-Name'), optional=True).with_properties(placeholder=L_("Your display name (informational)"))
-        openid = String.using(label=L_('OpenID'), optional=True).with_properties(placeholder=L_("Your OpenID address")).validated_by(URLValidator())
+        name = List.using(label=L_('Name')).of(RequiredText.using(label=L_('Name')).with_properties(placeholder=L_("The login name you want to use")))
+        display_name = OptionalText.using(label=L_('Display-Name')).with_properties(placeholder=L_("Your display name (informational)"))
+        openid = YourOpenID.using(optional=True)
         #timezones_keys = sorted(Locale('en').time_zones.keys())
         timezones_keys = [unicode(tz) for tz in pytz.common_timezones]
-        timezone = Enum.using(label=L_('Timezone')).valued(*timezones_keys)
+        timezone = Select.using(label=L_('Timezone')).valued(*timezones_keys)
         supported_locales = [Locale('en')] + app.babel_instance.list_translations()
         locales_available = sorted([(unicode(l), l.display_name) for l in supported_locales],
                                    key=lambda x: x[1])
         locales_keys = [l[0] for l in locales_available]
-        locale = Enum.using(label=L_('Locale')).with_properties(labels=dict(locales_available)).valued(*locales_keys)
-        submit = String.using(default=L_('Save'), optional=True)
+        locale = Select.using(label=L_('Locale')).with_properties(labels=dict(locales_available)).valued(*locales_keys)
+        submit = Submit.using(default=L_('Save'))
 
     class UserSettingsUIForm(Form):
         name = 'usersettings_ui'
         themes_available = sorted([(unicode(t.identifier), t.name) for t in get_themes_list()],
                                   key=lambda x: x[1])
         themes_keys = [t[0] for t in themes_available]
-        theme_name = Enum.using(label=L_('Theme name')).with_properties(labels=dict(themes_available)).valued(*themes_keys)
-        css_url = String.using(label=L_('User CSS URL'), optional=True).with_properties(placeholder=L_("Give the URL of your custom CSS (optional)")).validated_by(URLValidator())
-        edit_rows = Integer.using(label=L_('Editor size')).with_properties(placeholder=L_("Editor textarea height (0=auto)")).validated_by(Converted())
-        results_per_page = Integer.using(label=L_('History results per page')).with_properties(placeholder=L_("Number of results per page (0=no paging)")).validated_by(ValueAtLeast(0))
-        submit = String.using(default=L_('Save'), optional=True)
+        theme_name = Select.using(label=L_('Theme name')).with_properties(labels=dict(themes_available)).valued(*themes_keys)
+        css_url = URL.using(label=L_('User CSS URL'), optional=True).with_properties(placeholder=L_("Give the URL of your custom CSS (optional)"))
+        edit_rows = Natural.using(label=L_('Editor size')).with_properties(placeholder=L_("Editor textarea height (0=auto)"))
+        results_per_page = Natural.using(label=L_('History results per page')).with_properties(placeholder=L_("Number of results per page (0=no paging)"))
+        submit = Submit.using(default=L_('Save'))
 
     form_classes = dict(
         personal=UserSettingsPersonalForm,
@@ -1314,6 +1419,9 @@
     )
     forms = dict()
 
+    if not flaskg.user.valid:
+        return redirect(url_for('.login'))
+
     if request.method == 'POST':
         part = request.form.get('part')
         if part not in form_classes:
@@ -1330,9 +1438,9 @@
 
             # save response to a dict as we can't use HTTP redirects or flash() for XHR requests
             response = dict(
-                form = None,
-                flash = [],
-                redirect = None,
+                form=None,
+                flash=[],
+                redirect=None,
             )
 
             if form.validate():
@@ -1370,8 +1478,7 @@
                             # send verification mail
                             is_ok, msg = flaskg.user.mail_email_verification()
                             if is_ok:
-                                _logout()
-                                flaskg.user.save()
+                                flaskg.user.logout_session()
                                 response['flash'].append((_('Your account has been disabled because you changed your email address. Please see the email we sent to your address to reactivate it.'), "info"))
                                 response['redirect'] = url_for('.show_root')
                             else:
@@ -1456,7 +1563,7 @@
     return rev1, rev2
 
 
-@frontend.route('/+diffraw/<path:item_name>')
+@frontend.route('/+diffraw/<itemname:item_name>')
 def diffraw(item_name):
     # TODO get_item and get_revision calls may raise an AccessDenied.
     #      If this happens for get_item, don't show the diff at all
@@ -1470,7 +1577,7 @@
     return _diff_raw(item, rev1, rev2)
 
 
-@frontend.route('/+diff/<path:item_name>')
+@frontend.route('/+diff/<itemname:item_name>')
 def diff(item_name):
     item = flaskg.storage[item_name]
     bookmark_time = request.values.get('bookmark')
@@ -1479,7 +1586,7 @@
         # try to find the latest rev1 before bookmark <date>
         revs = sorted([(rev.meta[MTIME], rev.revid) for rev in item.iter_revs()], reverse=True)
         for mtime, revid in revs:
-            if mtime <= bookmark_time:
+            if mtime <= int(bookmark_time):
                 rev1 = revid
                 break
         else:
@@ -1522,7 +1629,7 @@
     rev_ids = [CURRENT]  # XXX TODO we need a reverse sorted list
     return render_template(item.diff_template,
                            item=item, item_name=item.name,
-                           diff_html=Markup(item._render_data_diff(oldrev, newrev)),
+                           diff_html=Markup(item.content._render_data_diff(oldrev, newrev)),
                            rev=item.rev,
                            first_rev_id=rev_ids[0],
                            last_rev_id=rev_ids[-1],
@@ -1540,7 +1647,7 @@
         item = Item.create(item.name, contenttype=commonmt, rev_id=newrev.revid)
     except AccessDenied:
         abort(403)
-    return item._render_data_diff_raw(oldrev, newrev)
+    return item.content._render_data_diff_raw(oldrev, newrev)
 
 
 @frontend.route('/+similar_names/<itemname:item_name>')
@@ -1663,6 +1770,8 @@
     :rtype: list
     :returns: list of matching item names, sorted by rank
     """
+    if not item_names:
+        return []
     # Match using case insensitive matching
     # Make mapping from lower item names to item names.
     lower = {}
@@ -1675,7 +1784,7 @@
 
     # Get all close matches
     all_matches = difflib.get_close_matches(item_name.lower(), lower.keys(),
-                                            len(lower), cutoff=0.6)
+                                            n=len(lower), cutoff=0.6)
 
     # Replace lower names with original names
     matches = []
@@ -1685,7 +1794,7 @@
     return matches
 
 
-@frontend.route('/+sitemap/<item_name>')
+@frontend.route('/+sitemap/<itemname:item_name>')
 def sitemap(item_name):
     """
     sitemap view shows item link structure, relative to current item
@@ -1790,8 +1899,38 @@
                            item_name=tag,
                            item_names=item_names)
 
+
+@frontend.route('/+template/<path:filename>')
+def template(filename):
+    """
+    serve a rendered template from <filename>
+
+    used for (but not limited to) translation of javascript / css / html
+    """
+    content = render_template(filename)
+    ct, enc = mimetypes.guess_type(filename)
+    response = make_response((content, 200, {'content-type': ct or 'text/plain;charset=utf-8'}))
+    if ct in ['application/javascript', 'text/css', 'text/html', ]:
+        # this is assuming that:
+        # * js / css / html templates rarely change (maybe just on sw updates)
+        # * we are using templates for these to translate them, translations rarely change
+        # * the rendered template output is just specific per user but does not change in time
+        #   or by other circumstances
+        cache_timeout = 24 * 3600  # 1 day
+        is_public = False  # expanded template may be different for each user (translation)
+    else:
+        # safe defaults:
+        cache_timeout = None
+        is_public = False
+    if cache_timeout is not None:
+        # set some cache control headers, so browsers do not request this again and again:
+        response.cache_control.public = is_public
+        response.cache_control.max_age = cache_timeout
+        response.expires = int(time.time() + cache_timeout)
+    return response
+
+
 @frontend.errorhandler(404)
 def page_not_found(e):
     return render_template('404.html',
                            item_name=e.description), 404
-
--- a/MoinMoin/apps/misc/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/misc/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -11,4 +11,3 @@
 from flask import Blueprint
 misc = Blueprint('misc', __name__, template_folder='templates')
 import MoinMoin.apps.misc.views
-
--- a/MoinMoin/apps/misc/views.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/misc/views.py	Sat Oct 06 17:46:24 2012 +0200
@@ -7,6 +7,7 @@
     Misc. stuff that doesn't fit into another view category.
 """
 
+import time
 
 from flask import Response
 from flask import current_app as app
@@ -25,8 +26,9 @@
     """
     Google (and others) XML sitemap
     """
-    def format_timestamp(dt):
-        return dt.strftime("%Y-%m-%dT%H:%M:%S+00:00")
+    def format_timestamp(t):
+        tm = time.gmtime(t)
+        return time.strftime("%Y-%m-%dT%H:%M:%S+00:00", tm)
 
     sitemap = []
     for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname):
@@ -67,4 +69,3 @@
     item_names = sorted([rev.name for rev in flaskg.storage.documents(wikiname=app.cfg.interwikiname)])
     content = render_template('misc/urls_names.txt', item_names=item_names)
     return Response(content, mimetype='text/plain')
-
--- a/MoinMoin/apps/serve/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/serve/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -13,4 +13,3 @@
 from flask import Blueprint
 serve = Blueprint('serve', __name__)
 import MoinMoin.apps.serve.views
-
--- a/MoinMoin/apps/serve/_tests/test_serve.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/serve/_tests/test_serve.py	Sat Oct 06 17:46:24 2012 +0200
@@ -20,4 +20,3 @@
             assert rv.status == '404 NOT FOUND'
             assert rv.headers['Content-Type'] == 'text/html'
             assert '<!DOCTYPE HTML' in rv.data
-
--- a/MoinMoin/apps/serve/views.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/apps/serve/views.py	Sat Oct 06 17:46:24 2012 +0200
@@ -36,4 +36,3 @@
         abort(404)
 
     return send_from_directory(base_path, filename)
-
--- a/MoinMoin/auth/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/auth/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -434,6 +434,7 @@
         trusted = session['user.trusted']
         auth_method = session['user.auth_method']
         auth_attribs = session['user.auth_attribs']
+        session_token = session['user.session_token']
         logging.debug("got from session: {0!r} {1!r} {2!r} {3!r}".format(itemid, trusted, auth_method, auth_attribs))
         logging.debug("current auth methods: {0!r}".format(app.cfg.auth_methods))
         if auth_method and auth_method in app.cfg.auth_methods:
@@ -441,6 +442,11 @@
                                 auth_method=auth_method,
                                 auth_attribs=auth_attribs,
                                 trusted=trusted)
+            if userobj.valid and not userobj.validate_session(session_token):
+                logging.debug("session token doesn't validate")
+                # Destroy current session since it's no longer valid.
+                userobj.logout_session(False)
+                # We didn't find user in session data.
+                userobj = None
     logging.debug("session started for user {0!r}".format(userobj))
     return userobj
-
--- a/MoinMoin/auth/_tests/test_auth.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/auth/_tests/test_auth.py	Sat Oct 06 17:46:24 2012 +0200
@@ -57,7 +57,7 @@
 def test_handle_login():
     # no messages in the beginning
     assert not flaskg._login_messages
-    test_user1 = handle_login(flaskg.user, login_username = 'test_user', login_password = 'test_password', stage = 'moin')
+    test_user1 = handle_login(flaskg.user, login_username='test_user', login_password='test_password', stage='moin')
     test_login_message = [u'Invalid username or password.']
     assert flaskg._login_messages == test_login_message
     assert test_user1.name0 == u'anonymous'
@@ -70,7 +70,7 @@
     givenauth_obj.user_name = u'Test_User'
     create_user(u'Test_User', u'test_pass', u'test@moinmoin.org')
     test_user, bool_value = givenauth_obj.request(flaskg.user)
-    test_user2 = handle_login(test_user, login_username = 'Test_User', login_password = 'test_pass', stage = 'moin')
+    test_user2 = handle_login(test_user, login_username='Test_User', login_password='test_pass', stage='moin')
     assert not flaskg._login_messages
     assert test_user2.name == [u'Test_User', ]
     assert test_user2.valid
@@ -81,4 +81,3 @@
     assert 'password=test_pass' in test_url
     assert 'stage=test_auth_name' in test_url
     assert 'login_submit=1' in test_url
-
--- a/MoinMoin/auth/_tests/test_http.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/auth/_tests/test_http.py	Sat Oct 06 17:46:24 2012 +0200
@@ -41,4 +41,3 @@
         test_user, bool_val = httpauthmoin_obj.request(flaskg.user)
         assert not test_user.valid
         assert test_user.name0 == u'anonymous'
-
--- a/MoinMoin/auth/_tests/test_ldap_login.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/auth/_tests/test_ldap_login.py	Sat Oct 06 17:46:24 2012 +0200
@@ -179,7 +179,6 @@
     slapd_config = SLAPD_CONFIG
     ldif_content = LDIF_CONTENT
 
-
     def setup_class(self):
         """ Create LDAP servers environment, start slapds """
         self.ldap_envs = []
@@ -233,4 +232,3 @@
         u2 = handle_login(None, username='usera', password='usera')
         assert u2 is not None
         assert u2.valid
-
--- a/MoinMoin/auth/_tests/test_log.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/auth/_tests/test_log.py	Sat Oct 06 17:46:24 2012 +0200
@@ -36,4 +36,3 @@
         assert test_user.name0 == u'anonymous'
         assert not test_user.valid
         assert bool_value
-
--- a/MoinMoin/auth/http.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/auth/http.py	Sat Oct 06 17:46:24 2012 +0200
@@ -70,4 +70,3 @@
         else:
             logging.debug("returning {0!r}".format(user_obj))
             return user_obj, True
-
--- a/MoinMoin/auth/ldap_login.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/auth/ldap_login.py	Sat Oct 06 17:46:24 2012 +0200
@@ -124,7 +124,6 @@
         username = kw.get('username')
         password = kw.get('password')
 
-
         # we require non-empty password as ldap bind does a anon (not password
         # protected) bind if the password is empty and SUCCEEDS!
         if not password:
@@ -265,4 +264,3 @@
         except:
             logging.exception("caught an exception, traceback follows...")
             return ContinueLogin(user_obj)
-
--- a/MoinMoin/auth/openidrp.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/auth/openidrp.py	Sat Oct 06 17:46:24 2012 +0200
@@ -101,7 +101,6 @@
                                                            openid_submit='1'
                                                           ))
 
-
             # not trusted
             return ContinueLogin(None, _('This OpenID provider is not trusted.'))
 
@@ -164,4 +163,3 @@
 
                 # returns a MultistageFormLogin
                 return MultistageFormLogin(form_html)
-
--- a/MoinMoin/auth/smb_mount.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/auth/smb_mount.py	Sat Oct 06 17:46:24 2012 +0200
@@ -49,7 +49,7 @@
         self.coding = coding
 
     def do_smb(self, username, password, login):
-        logging.debug("login={0} logout={1}: got name={2}".format(login, not login, username))
+        logging.debug("login={0} logout={1}: got name={2!r}".format(login, not login, username))
 
         import os, pwd, subprocess
         web_username = self.dir_user
@@ -94,4 +94,3 @@
         if user_obj and not user_obj.valid:
             self.do_smb(user_obj.name, None, False)
         return user_obj, True
-
--- a/MoinMoin/config/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/config/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -11,4 +11,3 @@
 from MoinMoin.constants.contenttypes import *
 from MoinMoin.constants.keys import *
 from MoinMoin.constants.misc import *
-
--- a/MoinMoin/config/_tests/test_defaultconfig.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/config/_tests/test_defaultconfig.py	Sat Oct 06 17:46:24 2012 +0200
@@ -37,4 +37,3 @@
                 assert result == (pw_error is None)
 
 coverage_modules = ['MoinMoin.config.default']
-
--- a/MoinMoin/config/default.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/config/default.py	Sat Oct 06 17:46:24 2012 +0200
@@ -24,7 +24,7 @@
 from MoinMoin import datastruct
 from MoinMoin.auth import MoinAuth
 from MoinMoin.util import plugins
-from MoinMoin.security import AccessControlList
+from MoinMoin.security import AccessControlList, DefaultSecurityPolicy
 
 
 class CacheClass(object):
@@ -113,15 +113,15 @@
         self.mail_enabled = self.mail_enabled and True or False
 
         if self.namespace_mapping is None:
-            raise error.ConfigurationError("No storage configuration specified! You need to define a namespace_mapping. " + \
+            raise error.ConfigurationError("No storage configuration specified! You need to define a namespace_mapping. "
                                            "For further reference, please see HelpOnStorageConfiguration.")
 
         if self.backend_mapping is None:
-            raise error.ConfigurationError("No storage configuration specified! You need to define a backend_mapping. " + \
+            raise error.ConfigurationError("No storage configuration specified! You need to define a backend_mapping. " +
                                            "For further reference, please see HelpOnStorageConfiguration.")
 
         if self.acl_mapping is None:
-            raise error.ConfigurationError("No acl configuration specified! You need to define a acl_mapping. " + \
+            raise error.ConfigurationError("No acl configuration specified! You need to define a acl_mapping. "
                                            "For further reference, please see HelpOnStorageConfiguration.")
 
         if self.secrets is None:  # admin did not setup a real secret
@@ -307,7 +307,7 @@
      "list of auth objects, to be called in this order (see HelpOnAuthentication)"),
     ('secrets', None, """Either a long shared secret string used for multiple purposes or a dict {"purpose": "longsecretstring", ...} for setting up different shared secrets for different purposes."""),
     ('SecurityPolicy',
-     None,
+     DefaultSecurityPolicy,
      "Class object hook for implementing security restrictions or relaxations"),
     ('endpoints_excluded',
      [],
@@ -338,7 +338,8 @@
         ('wikilink', 'frontend.global_history', dict(), L_('History'), L_('Global History')),
         ('wikilink', 'frontend.index', dict(), L_('Index'), L_('Global Index')),
         ('wikilink', 'frontend.global_tags', dict(), L_('Tags'), L_('Global Tags Index')),
-        ('wikilink', 'admin.index', dict(), L_('More'), L_('Administration & Docs')),
+        ('wikilink', 'admin.index_user', dict(), L_('User'), L_('User')),
+        ('wikilink', 'admin.index', dict(), L_('Admin'), L_('Administration & Docs')),
      ],
      'Data to create the navi_bar from. Users can add more items in their quick links in user preferences. You need to configure a list of tuples (css_class, endpoint, args, label, title). Use L_() for translating. [list of tuples]'),
 
@@ -369,9 +370,8 @@
         ('frontend.modify_item', L_('Modify'), L_('Edit or Upload'), True, ),
         ('special.supplementation', None, None, False, ),
         ('frontend.index', L_('Index'), L_('List sub-items'), False, ),
-        # The | character in the comments and transclusions lines below separate the off/on title (tooltip) values used by javascript
-        ('special.comments', L_('Comments'), L_('Show comments|Hide comments'), True, ),
-        ('special.transclusions', L_('Transclusions'), L_('Show transclusions|Hide transclusions'), True, ),
+        ('special.comments', L_('Comments'), L_('Hide comments'), True, ),
+        ('special.transclusions', L_('Transclusions'), L_('Show transclusions'), True, ),
         ('frontend.highlight_item', L_('Highlight'), L_('Show with Syntax-Highlighting'), True, ),
         ('frontend.show_item_meta', L_('Meta'), L_('Display Metadata'), True, ),
         ('frontend.quicklink_item', None, L_('Create or remove a navigation link to this item'), False, ),
@@ -415,13 +415,13 @@
     ('interwiki_map', {},
      "Dictionary of wiki_name -> wiki_url"),
     ('namespace_mapping', None,
-    "A list of tuples, each tuple containing: Namespace identifier, backend name. " + \
+    "A list of tuples, each tuple containing: Namespace identifier, backend name. " +
     "E.g.: [('', 'default')), ]. Please see HelpOnStorageConfiguration for further reference."),
     ('backend_mapping', None,
-    "A dictionary that maps backend names to backends. " + \
+    "A dictionary that maps backend names to backends. " +
     "E.g.: {'default': Backend(), }. Please see HelpOnStorageConfiguration for further reference."),
     ('acl_mapping', None,
-    "This needs to point to a list of tuples, each tuple containing: name prefix, acl protection to be applied to matching items. " + \
+    "This needs to point to a list of tuples, each tuple containing: name prefix, acl protection to be applied to matching items. " +
     "E.g.: [('', dict(default='All:read,write,create')), ]. Please see HelpOnStorageConfiguration for further reference."),
     ('create_storage', False, "Create (initialize) the storage backends before trying to use them."),
     ('create_index', False, "Create (initialize) the index before trying to use them."),
@@ -452,6 +452,7 @@
         css_url=None,
         mailto_author=False,
         edit_on_doubleclick=True,
+        scroll_page_after_edit=True,
         show_comments=False,
         want_trivial=False,
         disabled=False,
@@ -542,7 +543,7 @@
     )),
 
     'ns': ('Storage Namespaces',
-    "Storage namespaces can be defined for all sorts of data. All items sharing a common namespace as prefix" + \
+    "Storage namespaces can be defined for all sorts of data. All items sharing a common namespace as prefix" +
     "are then stored within the same backend. The common prefix for all data is ''.",
     (
       ('content', '/', "All content is by default stored below /, hence the prefix is ''."),  # Not really necessary. Just for completeness.
@@ -584,4 +585,3 @@
 
 _add_options_to_defconfig(options)
 _add_options_to_defconfig(options_no_group_name, False)
-
--- a/MoinMoin/conftest.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/conftest.py	Sat Oct 06 17:46:24 2012 +0200
@@ -51,10 +51,10 @@
         namespace_mapping=namespace_mapping,
         backend_mapping=backend_mapping,
         acl_mapping=acl_mapping,
-        create_storage = True, # create a fresh storage at each app start
-        destroy_storage = True, # kill all storage contents at app shutdown
-        create_index = True, # create a fresh index at each app start
-        destroy_index = True, # kill index contents at app shutdown
+        create_storage=True, # create a fresh storage at each app start
+        destroy_storage=True, # kill all storage contents at app shutdown
+        create_index=True, # create a fresh index at each app start
+        destroy_index=True, # kill index contents at app shutdown
     )
     app = create_app_ext(flask_config_dict=dict(SECRET_KEY='foobarfoobar'),
                          moin_config_class=given_config,
@@ -103,7 +103,6 @@
         if hasattr(self._obj, 'im_self'):
             self._obj.im_self.app = self.app
 
-
     def teardown(self):
         super(MoinTestFunction, self).teardown()
 
@@ -113,7 +112,7 @@
 
 def pytest_pycollect_makeitem(__multicall__, collector, name, obj):
     if collector.funcnamefilter(name) and inspect.isfunction(obj):
-        return MoinTestFunction(name, parent = collector)
+        return MoinTestFunction(name, parent=collector)
 
 def pytest_pyfunc_call(pyfuncitem):
     """hook to intercept generators and run them as a single test items"""
@@ -130,4 +129,3 @@
         if coverage is not None:
             coverage_modules.update(getattr(self.obj, 'coverage_modules', []))
         return super(Module, self).run(*args, **kwargs)
-
--- a/MoinMoin/constants/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/constants/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -4,4 +4,3 @@
 """
 MoinMoin - modules with constant definitions
 """
-
--- a/MoinMoin/constants/chartypes.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/constants/chartypes.py	Sat Oct 06 17:46:24 2012 +0200
@@ -6,6 +6,3 @@
 chars_digits = u"\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039\u00b2\u00b3\u00b9\u0660\u0661\u0662\u0663\u0664\u0665\u0666\u0667\u0668\u0669\u06f0\u06f1\u06f2\u06f3\u06f4\u06f5\u06f6\u06f7\u06f8\u06f9\u0966\u0967\u0968\u0969\u096a\u096b\u096c\u096d\u096e\u096f\u09e6\u09e7\u09e8\u09e9\u09ea\u09eb\u09ec\u09ed\u09ee\u09ef\u0a66\u0a67\u0a68\u0a69\u0a6a\u0a6b\u0a6c\u0a6d\u0a6e\u0a6f\u0ae6\u0ae7\u0ae8\u0ae9\u0aea\u0aeb\u0aec\u0aed\u0aee\u0aef\u0b66\u0b67\u0b68\u0b69\u0b6a\u0b6b\u0b6c\u0b6d\u0b6e\u0b6f\u0be7\u0be8\u0be9\u0bea\u0beb\u0bec\u0bed\u0bee\u0bef\u0c66\u0c67\u0c68\u0c69\u0c6a\u0c6b\u0c6c\u0c6d\u0c6e\u0c6f\u0ce6\u0ce7\u0ce8\u0ce9\u0cea\u0ceb\u0cec\u0ced\u0cee\u0cef\u0d66\u0d67\u0d68\u0d69\u0d6a\u0d6b\u0d6c\u0d6d\u0d6e\u0d6f\u0e50\u0e51\u0e52\u0e53\u0e54\u0e55\u0e56\u0e57\u0e58\u0e59\u0ed0\u0ed1\u0ed2\u0ed3\u0ed4\u0ed5\u0ed6\u0ed7\u0ed8\u0ed9\u0f20\u0f21\u0f22\u0f23\u0f24\u0f25\u0f26\u0f27\u0f28\u0f29\u1040\u1041\u1042\u1043\u1044\u1045\u1046\u1047\u1048\u1049\u1369\u136a\u136b\u136c\u136d\u136e\u136f\u1370\u1371\u17e0\u17e1\u17e2\u17e3\u17e4\u17e5\u17e6\u17e7\u17e8\u17e9\u1810\u1811\u1812\u1813\u1814\u1815\u1816\u1817\u1818\u1819\u2070\u2074\u2075\u2076\u2077\u2078\u2079\u2080\u2081\u2082\u2083\u2084\u2085\u2086\u2087\u2088\u2089\u2460\u2461\u2462\u2463\u2464\u2465\u2466\u2467\u2468\u2474\u2475\u2476\u2477\u2478\u2479\u247a\u247b\u247c\u2488\u2489\u248a\u248b\u248c\u248d\u248e\u248f\u2490\u24ea\u24f5\u24f6\u24f7\u24f8\u24f9\u24fa\u24fb\u24fc\u24fd\u2776\u2777\u2778\u2779\u277a\u277b\u277c\u277d\u277e\u2780\u2781\u2782\u2783\u2784\u2785\u2786\u2787\u2788\u278a\u278b\u278c\u278d\u278e\u278f\u2790\u2791\u2792\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19"
 
 chars_spaces = u"\u0009\u000a\u000b\u000c\u000d\u001c\u001d\u001e\u001f\u0020\u0085\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000"
-
-
-
--- a/MoinMoin/constants/contenttypes.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/constants/contenttypes.py	Sat Oct 06 17:46:24 2012 +0200
@@ -18,50 +18,11 @@
 CONTENTTYPE_USER = u'application/x.moin.userprofile'
 CONTENTTYPE_DEFAULT = u'application/octet-stream'
 
-# structure for contenttype groups
-CONTENTTYPE_GROUPS = [
-    ('markup text items', [
-        ('text/x.moin.wiki;charset=utf-8', 'Wiki (MoinMoin)'),
-        ('text/x.moin.creole;charset=utf-8', 'Wiki (Creole)'),
-        ('text/x-mediawiki;charset=utf-8', 'Wiki (MediaWiki)'),
-        ('text/x-rst;charset=utf-8', 'ReST'),
-        ('application/docbook+xml;charset=utf-8', 'DocBook'),
-        ('text/html;charset=utf-8', 'HTML'),
-    ]),
-    ('other text items', [
-        ('text/plain;charset=utf-8', 'plain text'),
-        ('text/x-diff;charset=utf-8', 'diff/patch'),
-        ('text/x-python;charset=utf-8', 'python code'),
-        ('text/csv;charset=utf-8', 'csv'),
-        ('text/x-irclog;charset=utf-8', 'IRC log'),
-    ]),
-    ('image items', [
-        ('image/jpeg', 'JPEG'),
-        ('image/png', 'PNG'),
-        ('image/svg+xml', 'SVG'),
-    ]),
-    ('audio items', [
-        ('audio/wave', 'WAV'),
-        ('audio/ogg', 'OGG'),
-        ('audio/mpeg', 'MP3'),
-        ('audio/webm', 'WebM'),
-    ]),
-    ('video items', [
-        ('video/ogg', 'OGG'),
-        ('video/webm', 'WebM'),
-        ('video/mp4', 'MP4'),
-    ]),
-    ('drawing items', [
-        ('application/x-twikidraw', 'TDRAW'),
-        ('application/x-anywikidraw', 'ADRAW'),
-        ('application/x-svgdraw', 'SVGDRAW'),
-    ]),
-    ('other items', [
-        ('application/pdf', 'PDF'),
-        ('application/zip', 'ZIP'),
-        ('application/x-tar', 'TAR'),
-        ('application/x-gtar', 'TGZ'),
-        ('application/octet-stream', 'binary file'),
-    ]),
-]
 
+GROUP_MARKUP_TEXT = 'markup text items'
+GROUP_OTHER_TEXT = 'other text items'
+GROUP_IMAGE = 'image items'
+GROUP_AUDIO = 'audio items'
+GROUP_VIDEO = 'video items'
+GROUP_DRAWING = 'drawing items'
+GROUP_OTHER = 'other items'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/constants/forms.py	Sat Oct 06 17:46:24 2012 +0200
@@ -0,0 +1,33 @@
+# Copyright: 2012 MoinMoin:CheerXiao
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+MoinMoin - Flatland form related constants
+"""
+
+# Widget types
+
+WIDGET_TEXT = u'text' # single-line text
+WIDGET_MULTILINE_TEXT = u'multiline_text'
+WIDGET_URL = u'url'
+WIDGET_EMAIL = u'email'
+WIDGET_PASSWORD = u'password'
+WIDGET_CHECKBOX = u'checkbox'
+WIDGET_INLINE_CHECKBOX = u'inline_checkbox'
+WIDGET_ANY_INTEGER = u'any_integer'
+WIDGET_SMALL_NATURAL = u'small_natural'
+WIDGET_DATETIME = u'datetime'
+
+WIDGET_FILE = u'file'
+WIDGET_SEARCH = u'search'
+WIDGET_SUBMIT = u'submit'
+WIDGET_HIDDEN = u'hidden'
+
+WIDGET_SELECT = u'select'
+WIDGET_MULTI_SELECT = u'multi_select'
+
+WIDGET_READONLY_STRING_LIST = u'readonly_string_list'
+WIDGET_READONLY_ITEM_LINK_LIST = u'readonly_item_link_list'
+
+# CSS Classes
+CLASS_BUTTON = u'button'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/constants/itemtypes.py	Sat Oct 06 17:46:24 2012 +0200
@@ -0,0 +1,13 @@
+# Copyright: 2012 MoinMoin:CheerXiao
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+MoinMoin - itemtype related constants
+"""
+
+ITEMTYPE_NONEXISTENT = u'nonexistent'
+ITEMTYPE_USERPROFILE = u'userprofile'
+ITEMTYPE_DEFAULT = u'default'  # == wiki-like
+ITEMTYPE_TICKET = u'ticket'
+ITEMTYPE_BLOG = u'blog'
+ITEMTYPE_BLOGENTRY = u'blogentry'
--- a/MoinMoin/constants/keys.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/constants/keys.py	Sat Oct 06 17:46:24 2012 +0200
@@ -5,6 +5,10 @@
 MoinMoin - meta data key / index field name related constants
 """
 
+# IMPORTANT: until we require a python >= 2.6.5, we need to keep the keys as
+#            str (not unicode), because of "Issue #4978: Passing keyword
+#            arguments as unicode strings is now allowed." (from 2.6.5 chglog)
+
 # metadata keys
 NAME = "name"
 NAME_OLD = "name_old"
@@ -28,8 +32,10 @@
 SOMEDICT = "somedict"
 
 CONTENTTYPE = "contenttype"
+ITEMTYPE = "itemtype"
 SIZE = "size"
 LANGUAGE = "language"
+EXTERNALLINKS = "externallinks"
 ITEMLINKS = "itemlinks"
 ITEMTRANSCLUSIONS = "itemtransclusions"
 TAGS = "tags"
@@ -41,6 +47,7 @@
 MTIME = "mtime"
 EXTRA = "extra"
 COMMENT = "comment"
+SUMMARY = "summary"
 
 # we need a specific hash algorithm to store hashes of revision data into meta
 # data. meta[HASH_ALGORITHM] = hash(rev_data, HASH_ALGORITHM)
@@ -71,17 +78,35 @@
 SUBSCRIBED_ITEMS = "subscribed_items"
 BOOKMARKS = "bookmarks"
 QUICKLINKS = "quicklinks"
+SESSION_KEY = "session_key"
+SESSION_TOKEN = "session_token"
 RECOVERPASS_KEY = "recoverpass_key"
 EDIT_ON_DOUBLECLICK = "edit_on_doubleclick"
+SCROLL_PAGE_AFTER_EDIT = "scroll_page_after_edit"
 SHOW_COMMENTS = "show_comments"
 MAILTO_AUTHOR = "mailto_author"
+CSS_URL = "css_url"
+EDIT_ROWS = "edit_rows"
 RESULTS_PER_PAGE = "results_per_page"
 DISABLED = "disabled"
 
 # in which backend is some revision stored?
 BACKENDNAME = "backendname"
 
+USEROBJ_ATTRS = [
+    # User objects proxy these attributes of the UserProfile objects:
+    NAME, DISABLED, ITEMID, DISPLAY_NAME, ENC_PASSWORD, EMAIL, OPENID,
+    MAILTO_AUTHOR, SHOW_COMMENTS, RESULTS_PER_PAGE, EDIT_ON_DOUBLECLICK,
+    EDIT_ROWS, THEME_NAME, LOCALE, TIMEZONE, SUBSCRIBED_ITEMS, QUICKLINKS,
+    CSS_URL,
+]
+
+# keys for blog homepages
+LOGO = "logo"
+SUPERTAGS = "supertags"
+# keys for blog entries
+PTIME = "ptime"
+
 # index names
 LATEST_REVS = 'latest_revs'
 ALL_REVS = 'all_revs'
-
--- a/MoinMoin/constants/misc.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/constants/misc.py	Sat Oct 06 17:46:24 2012 +0200
@@ -44,4 +44,3 @@
                'notes',
                'rtp', 'rtsp', 'rtcp',
               ]
-
--- a/MoinMoin/constants/rights.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/constants/rights.py	Sat Oct 06 17:46:24 2012 +0200
@@ -21,10 +21,13 @@
 # admin means to be able to change, add, remove ACLs (change meta[ACL])
 ADMIN = 'admin'
 
-# read means to be able to read revision data
+# read means to be able to read revision data, unconditionally
 # TODO: define revision meta read behaviour
 READ = 'read'
 
+# pubread means to be able to read revision data when published
+PUBREAD = 'pubread'
+
 # write means to be able to change meta/data by creating a new revision,
 # so the previous data is still there, unchanged.
 WRITE = 'write'
@@ -37,5 +40,4 @@
 DESTROY = 'destroy'
 
 # rights that control access to operations on contents
-ACL_RIGHTS_CONTENTS = [READ, WRITE, CREATE, ADMIN, DESTROY, ]
-
+ACL_RIGHTS_CONTENTS = [READ, PUBREAD, WRITE, CREATE, ADMIN, DESTROY, ]
--- a/MoinMoin/constants/tools/chartypes_create.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/constants/tools/chartypes_create.py	Sat Oct 06 17:46:24 2012 +0200
@@ -43,4 +43,3 @@
 
 if __name__ == '__main__':
     main()
-
--- a/MoinMoin/converter/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -19,55 +19,30 @@
 """
 
 
+from collections import namedtuple
+
 from ..util.registry import RegistryBase
 from ..util.pysupport import load_package_modules
 
 
 class RegistryConverter(RegistryBase):
-    class Entry(object):
-        def __init__(self, factory, type_input, type_output, priority):
-            self.factory = factory
-            self.type_input = type_input
-            self.type_output = type_output
-            self.priority = priority
-
-        def __call__(self, type_input, type_output, kw):
+    class Entry(namedtuple('Entry', 'factory type_input type_output priority')):
+        def __call__(self, type_input, type_output, **kw):
             if (self.type_output.issupertype(type_output) and
                 self.type_input.issupertype(type_input)):
                 return self.factory(type_input, type_output, **kw)
 
-        def __eq__(self, other):
-            if isinstance(other, self.__class__):
-                return (self.factory == other.factory and
-                        self.type_input == other.type_input and
-                        self.type_output == other.type_output and
-                        self.priority == other.priority)
-            return NotImplemented
-
         def __lt__(self, other):
             if isinstance(other, self.__class__):
-                if self.priority < other.priority:
-                    return True
                 if self.type_output != other.type_output:
                     return other.type_output.issupertype(self.type_output)
                 if self.type_input != other.type_input:
                     return other.type_input.issupertype(self.type_input)
+                if self.priority != other.priority:
+                    return self.priority < other.priority
                 return False
             return NotImplemented
 
-        def __repr__(self):
-            return '<{0}: input {1}, output {2}, prio {3} [{4!r}]>' % (self.__class__.__name__,
-                    self.type_input,
-                    self.type_output,
-                    self.priority,
-                    self.factory)
-
-    def get(self, type_input, type_output, **kw):
-        for entry in self._entries:
-            conv = entry(type_input, type_output, kw)
-            if conv is not None:
-                return conv
-
     def register(self, factory, type_input, type_output, priority=RegistryBase.PRIORITY_MIDDLE):
         """
         Register a factory
@@ -79,4 +54,3 @@
 
 default_registry = RegistryConverter()
 load_package_modules(__name__, __path__)
-
--- a/MoinMoin/converter/_args.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_args.py	Sat Oct 06 17:46:24 2012 +0200
@@ -65,4 +65,3 @@
             yield value
         for value in self.keyword.itervalues():
             yield value
-
--- a/MoinMoin/converter/_args_wiki.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_args_wiki.py	Sat Oct 06 17:46:24 2012 +0200
@@ -83,4 +83,3 @@
         ret.append(key + u'=' + value)
 
     return u' '.join(ret)
-
--- a/MoinMoin/converter/_table.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_table.py	Sat Oct 06 17:46:24 2012 +0200
@@ -38,4 +38,3 @@
             table_body.append(table_row)
         table.append(table_body)
         return table
-
--- a/MoinMoin/converter/_tests/test__args.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_tests/test__args.py	Sat Oct 06 17:46:24 2012 +0200
@@ -94,4 +94,3 @@
     assert l[1] == 'both'
     assert True in l
     assert False in l
-
--- a/MoinMoin/converter/_tests/test__wiki_macro.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_tests/test__wiki_macro.py	Sat Oct 06 17:46:24 2012 +0200
@@ -116,4 +116,3 @@
         result = self.conv.parser(name, args, text)
         if output is not None or result is not None:
             assert self.serialize(result) == output
-
--- a/MoinMoin/converter/_tests/test_creole_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_tests/test_creole_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -261,4 +261,3 @@
     def do(self, input, output, args={}):
         out = self.conv(input, 'text/x.moin.creole;charset=utf-8', **args)
         assert self.serialize(out) == output
-
--- a/MoinMoin/converter/_tests/test_docbook_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_tests/test_docbook_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -429,4 +429,3 @@
         ]
         for i in data:
             yield (self.do_nonamespace, ) + i
-
--- a/MoinMoin/converter/_tests/test_html_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_tests/test_html_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -43,7 +43,6 @@
         out.write(f.write, namespaces=self.namespaces, )
         return self.output_re.sub(u'', f.getvalue())
 
-
     def do(self, input, path):
         string_to_parse = self.handle_input(input, args={})
         logging.debug("After the HTML_IN conversion : {0}".format(string_to_parse))
--- a/MoinMoin/converter/_tests/test_include.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_tests/test_include.py	Sat Oct 06 17:46:24 2012 +0200
@@ -9,7 +9,7 @@
 import pytest
 
 from MoinMoin.converter.include import *
-from MoinMoin.items import MoinWiki
+from MoinMoin.items import Item
 from MoinMoin.config import CONTENTTYPE
 from MoinMoin._tests import wikiconfig, update_item
 
@@ -65,20 +65,20 @@
 
     def test_IncludeHandlesCircularRecursion(self):
         # issue #80
-        # we use MoinWiki items to make tests simple
+        # we use text/x.moin.wiki markup to make tests simple
         update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{page2}}')
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{page3}}')
         update_item(u'page3', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{page4}}')
         update_item(u'page4', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{page2}}')
 
-        page1 = MoinWiki.create(u'page1')
-        rendered = page1._render_data()
+        page1 = Item.create(u'page1')
+        rendered = page1.content._render_data()
         # an error message will follow strong tag
         assert '<strong class="moin-error">' in rendered
 
     def test_ExternalInclude(self):
         update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{http://moinmo.in}}')
-        rendered = MoinWiki.create(u'page1')._render_data()
+        rendered = Item.create(u'page1').content._render_data()
         assert '<object class="moin-http moin-transclusion" data="http://moinmo.in" data-href="http://moinmo.in">http://moinmo.in</object>' in rendered
 
     def test_InlineInclude(self):
@@ -86,33 +86,32 @@
         update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Content of page2 is "{{page2}}".')
 
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Single line')
-        rendered = MoinWiki.create(u'page1')._render_data()
+        rendered = Item.create(u'page1').content._render_data()
         assert '<p>Content of page2 is "<span class="moin-transclusion" data-href="/page2">Single line</span>".</p>' in rendered
 
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Two\n\nParagraphs')
-        rendered = MoinWiki.create(u'page1')._render_data()
+        rendered = Item.create(u'page1').content._render_data()
         assert '<p>Content of page2 is "</p><div class="moin-transclusion" data-href="/page2"><p>Two</p><p>Paragraphs</p></div><p>".</p></div>' in rendered
 
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"this text contains ''italic'' string")
-        rendered = MoinWiki.create(u'page1')._render_data()
+        rendered = Item.create(u'page1').content._render_data()
         assert 'Content of page2 is "<span class="moin-transclusion" data-href="/page2">this text contains <em>italic</em>' in rendered
 
         update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Content of page2 is\n\n{{page2}}')
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"Single Line")
-        rendered = MoinWiki.create(u'page1')._render_data()
+        rendered = Item.create(u'page1').content._render_data()
         assert '<p>Content of page2 is</p><p><span class="moin-transclusion" data-href="/page2">Single Line</span></p>' in rendered
-        #           '<p>Content of page2 is</p><p><span class="moin-transclusion" data-href="http://127.0.0.1:8080/page2">Single Line</span></p>'
 
         update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Content of page2 is "{{page2}}"')
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"|| table || cell ||")
-        rendered = MoinWiki.create(u'page1')._render_data()
+        rendered = Item.create(u'page1').content._render_data()
         assert 'Content of page2 is "</p>' in rendered
         assert '<table>' in rendered
         assert rendered.count('<table>') == 1
 
         update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'Content of page2 is "{{page2}}"')
         update_item(u'page2', {CONTENTTYPE: u'text/x.moin.wiki'}, u"|| this || has ||\n|| two || rows ||")
-        rendered = MoinWiki.create(u'page1')._render_data()
+        rendered = Item.create(u'page1').content._render_data()
         assert 'Content of page2 is "</p>' in rendered
         assert '<table>' in rendered
         assert rendered.count('<table>') == 1
@@ -122,11 +121,11 @@
         update_item(u'logo', {CONTENTTYPE: u'image/png'}, u'')
 
         update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{logo}}')
-        rendered = MoinWiki.create(u'page1')._render_data()
+        rendered = Item.create(u'page1').content._render_data()
         assert '<img alt="logo" class="moin-transclusion"' in rendered
 
         # <p /> is not valid html5; should be <p></p>. to be valid.  Even better, there should be no empty p's.
         update_item(u'page1', {CONTENTTYPE: u'text/x.moin.wiki'}, u'{{logo}}{{logo}}')
-        rendered = MoinWiki.create(u'page1')._render_data()
+        rendered = Item.create(u'page1').content._render_data()
         assert '<p />' not in rendered
         assert '<p></p>' not in rendered
--- a/MoinMoin/converter/_tests/test_link.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_tests/test_link.py	Sat Oct 06 17:46:24 2012 +0200
@@ -65,6 +65,16 @@
         for i in pairs:
             yield (self._do_wikilocal, ) + i
 
+    def test_wikiexternal(self):
+        pairs = [
+            ('http://moinmo.in/',
+             'http://moinmo.in/'),
+            ('mailto:foo.bar@example.org',
+             'mailto:foo.bar@example.org'),
+        ]
+        for i in pairs:
+            yield (self._do_wikiexternal, ) + i
+
     def _do_wiki(self, input, output, skip=None):
         if skip:
             pytest.skip(skip)
@@ -79,6 +89,14 @@
         self.conv.handle_wikilocal_links(elem, Iri(input), Iri(page))
         assert elem.get(xlink.href) == output
 
+    def _do_wikiexternal(self, input, output, skip=None):
+        if skip:
+            pytest.skip(skip)
+        elem = ET.Element(None)
+        self.conv.handle_external_links(elem, Iri(input))
+        href = elem.get(xlink.href)
+        assert href == output
+
 
 class TestConverterRefs(object):
     def setup_class(self):
@@ -99,8 +117,9 @@
         """
         transclusions_expected = [u"moin_transcluded", u"moin2_transcluded"]
         links_expected = [u"moin_linked", u"moin2_linked"]
+        external_expected = []
 
-        self.runItemTest(tree_xml, links_expected, transclusions_expected)
+        self.runItemTest(tree_xml, links_expected, transclusions_expected, external_expected)
 
     def testRelativeItems(self):
         tree_xml = u"""
@@ -112,17 +131,32 @@
         """
         transclusions_expected = [u"Home/Subpage/moin2_transcluded", u"moin_transcluded"]
         links_expected = [u"moin_linked", u"Home/Subpage/moin2_linked"]
+        external_expected = []
 
-        self.runItemTest(tree_xml, links_expected, transclusions_expected)
+        self.runItemTest(tree_xml, links_expected, transclusions_expected, external_expected)
 
-    def runItemTest(self, tree_xml, links_expected, transclusions_expected):
+    def testExternal(self):
+        tree_xml = u"""
+        <ns0:page ns0:page-href="wiki:///Home/Subpage" xmlns:ns0="http://moinmo.in/namespaces/page" xmlns:ns1="http://www.w3.org/1999/xlink" xmlns:ns2="http://www.w3.org/2001/XInclude">
+        <ns0:body><ns0:p><ns0:a ns1:href="http://example.org/">test</ns0:a>
+        <ns0:a ns1:href="mailto:foo.bar@example.org">test</ns0:a>
+        </ns0:p></ns0:body></ns0:page>
+        """
+        transclusions_expected = []
+        links_expected = []
+        external_expected = [u"http://example.org/", u"mailto:foo.bar@example.org"]
+
+        self.runItemTest(tree_xml, links_expected, transclusions_expected, external_expected)
+
+    def runItemTest(self, tree_xml, links_expected, transclusions_expected, external_expected):
         tree = ET.XML(tree_xml)
         self.converter(tree)
         links_result = self.converter.get_links()
         transclusions_result = self.converter.get_transclusions()
+        external_result = self.converter.get_external_links()
 
         # sorting instead of sets
         # so that we avoid deduplicating duplicated items in the result
         assert sorted(links_result) == sorted(links_expected)
         assert sorted(transclusions_result) == sorted(transclusions_expected)
-
+        assert sorted(external_result) == sorted(external_expected)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/converter/_tests/test_markdown_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -0,0 +1,145 @@
+# Copyright: 2008 MoinMoin:BastianBlank
+# Copyright: 2012 MoinMoin:AndreasKloeckner
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+MoinMoin - Tests for MoinMoin.converter.markdown_in
+"""
+
+
+import re
+
+from MoinMoin.util.tree import moin_page, xlink
+
+from ..markdown_in import Converter
+
+
+class TestConverter(object):
+    namespaces = {
+        moin_page: '',
+        xlink: 'xlink',
+    }
+
+    output_re = re.compile(r'\s+xmlns(:\S+)?="[^"]+"')
+
+    def setup_class(self):
+        self.conv = Converter()
+
+    def test_base(self):
+        data = [
+            (u'Text',
+                '<p>Text</p>'),
+            (u'Text\nTest',
+                '<p>Text\nTest</p>'),
+            (u'Text\n\nTest',
+                '<p>Text</p>\n<p>Test</p>'),
+            (u'<http://moinmo.in/>',
+                '<p><a xlink:href="http://moinmo.in/">http://moinmo.in/</a></p>'),
+            (u'[yo](javascript:alert("xss"))',
+                '<p>javascript:alert("xss")</p>'),
+            (u'[MoinMoin](http://moinmo.in/)',
+                '<p><a xlink:href="http://moinmo.in/">MoinMoin</a></p>'),
+            (u'----',
+                '<separator />'),
+        ]
+        for i in data:
+            yield (self.do, ) + i
+
+    def test_emphasis(self):
+        data = [
+            (u'*Emphasis*',
+                '<p><emphasis>Emphasis</emphasis></p>'),
+            (u'_Emphasis_',
+                '<p><emphasis>Emphasis</emphasis></p>'),
+            (u'**Strong**',
+                '<p><strong>Strong</strong></p>'),
+            (u'_**Both**_',
+                '<p><emphasis><strong>Both</strong></emphasis></p>'),
+            (u'**_Both_**',
+                '<p><strong><emphasis>Both</emphasis></strong></p>'),
+        ]
+        for i in data:
+            yield (self.do, ) + i
+
+    def test_escape(self):
+        data = [
+            (u'http://moinmo.in/',
+                '<p>http://moinmo.in/</p>'),
+            (u'\[escape](yo)',
+                '<p>[escape](yo)</p>'),
+            (u'\*yo\*',
+                '<p>*yo*</p>'),
+        ]
+        for i in data:
+            yield (self.do, ) + i
+
+    def test_heading(self):
+        data = [
+            (u'# Heading 1',
+                '<h outline-level="1">Heading 1</h>'),
+            (u'## Heading 2',
+                '<h outline-level="2">Heading 2</h>'),
+            (u'### Heading 3',
+                '<h outline-level="3">Heading 3</h>'),
+            (u'#### Heading 4',
+                '<h outline-level="4">Heading 4</h>'),
+            (u'##### Heading 5',
+                '<h outline-level="5">Heading 5</h>'),
+            (u'###### Heading 6',
+                '<h outline-level="6">Heading 6</h>'),
+            (u'# Heading 1 #',
+                '<h outline-level="1">Heading 1</h>'),
+            (u'## Heading 2 ##',
+                '<h outline-level="2">Heading 2</h>'),
+            (u'### Heading 3 ###',
+                '<h outline-level="3">Heading 3</h>'),
+            (u'#### Heading 4 ####',
+                '<h outline-level="4">Heading 4</h>'),
+            (u'##### Heading 5 #####',
+                '<h outline-level="5">Heading 5</h>'),
+            (u'###### Heading 6 ######',
+                '<h outline-level="6">Heading 6</h>'),
+            (u'Heading 1\n=========\nHeading 2\n---------\n',
+                '<h outline-level="1">Heading 1</h>\n<h outline-level="2">Heading 2</h>'),
+            (u'Heading 1\n---------\n',
+                '<h outline-level="2">Heading 1</h>'),
+            (u'Heading\n=======\n\nxxxx',
+                '<h outline-level="1">Heading</h>\n<p>xxxx</p>'),
+        ]
+        for i in data:
+            yield (self.do, ) + i
+
+    def test_list(self):
+        data = [
+            (u'* Item',
+                '<list item-label-generate="unordered">\n<list-item><list-item-body>Item</list-item-body></list-item>\n</list>'),
+            (u'* Item\nItem',
+                '<list item-label-generate="unordered">\n<list-item><list-item-body>Item\nItem</list-item-body></list-item>\n</list>'),
+            (u'* Item 1\n* Item 2',
+                '<list item-label-generate="unordered">\n<list-item><list-item-body>Item 1</list-item-body></list-item>\n<list-item><list-item-body>Item 2</list-item-body></list-item>\n</list>'),
+            (u'* Item 1\n    * Item 1.2\n* Item 2',
+                '<list item-label-generate="unordered">\n<list-item><list-item-body>Item 1<list item-label-generate="unordered">\n<list-item><list-item-body>Item 1.2</list-item-body></list-item>\n</list>\n</list-item-body></list-item>\n<list-item><list-item-body>Item 2</list-item-body></list-item>\n</list>'),
+            (u'* List 1\n\nyo\n\n\n* List 2',
+                '<list item-label-generate="unordered">\n<list-item><list-item-body>List 1</list-item-body></list-item>\n</list>\n<p>yo</p>\n<list item-label-generate="unordered">\n<list-item><list-item-body>List 2</list-item-body></list-item>\n</list>'),
+            (u'8. Item',
+                '<list item-label-generate="ordered">\n<list-item><list-item-body>Item</list-item-body></list-item>\n</list>'),
+        ]
+        for i in data:
+            yield (self.do, ) + i
+
+    def serialize(self, elem, **options):
+        from StringIO import StringIO
+        buffer = StringIO()
+        elem.write(buffer.write, namespaces=self.namespaces, **options)
+        return self.output_re.sub(u'', buffer.getvalue())
+
+    def do(self, input, output, args={}):
+        out = self.conv(input, 'text/x-markdown;charset=utf-8', **args)
+        got_output = self.serialize(out)
+        desired_output = "<page><body>\n%s\n</body></page>" % output
+        print '------------------------------------'
+        print "WANTED:"
+        print desired_output
+        print "GOT:"
+        print got_output
+        assert got_output == desired_output
--- a/MoinMoin/converter/_tests/test_moinwiki19_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_tests/test_moinwiki19_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -37,4 +37,3 @@
         ]
         for i in data:
             yield (self.do, ) + i
-
--- a/MoinMoin/converter/_tests/test_moinwiki_in_out.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_tests/test_moinwiki_in_out.py	Sat Oct 06 17:46:24 2012 +0200
@@ -10,7 +10,7 @@
 
 
 import pytest
-pytest.skip("too much brokenness here, please help fixing this") # XXX TODO
+# failing tests are commented out and need to be fixed XXX TODO
 
 import re
 
@@ -22,7 +22,7 @@
 
 class TestConverter(object):
 
-    input_namespaces =  'xmlns="{0}" xmlns:page="{1}" xmlns:xlink="{2}"'.format(moin_page.namespace, moin_page.namespace, xlink.namespace)
+    input_namespaces = 'xmlns="{0}" xmlns:page="{1}" xmlns:xlink="{2}"'.format(moin_page.namespace, moin_page.namespace, xlink.namespace)
 
     namespaces = {
         moin_page.namespace: 'page',
@@ -50,7 +50,7 @@
             (u"~-smaller-~\n", '~-smaller-~\n'),
             (u"^super^script\n", '^super^script\n'),
             (u",,sub,,script\n", ',,sub,,script\n'),
-            (u"## comment\n", "## comment\n"),
+            # (u"## comment\n", "## comment\n"), # can not work -- "## comments" are not pushed to DOM
             (u"#ANY any", "#ANY any\n"),
         ]
         for i in data:
@@ -59,7 +59,7 @@
     def test_macros(self):
         data = [
             (u"<<Anchor(anchorname)>>", '<<Anchor(anchorname)>>\n'),
-            (u"<<MonthCalendar(,,12)>>", '<<MonthCalendar(,,12)>>\n'),
+            # (u"<<MonthCalendar(,,12)>>", '<<MonthCalendar(,,12)>>\n'), # MonthCalendar macro not implemented
             (u"<<FootNote(test)>>", "<<FootNote(test)>>\n"),
             (u"<<TableOfContents(2)>>", "<<TableOfContents(2)>>\n"),
             (u"<<TeudView()>>", "<<TeudView()>>\n"),
@@ -71,8 +71,8 @@
         data = [
             (u"{{{#!wiki comment/dotted\nThis is a wiki parser.\n\nIts visibility gets toggled the same way.\n}}}", u"{{{#!wiki comment/dotted\nThis is a wiki parser.\n\nIts visibility gets toggled the same way.\n}}}\n"),
             (u"{{{#!wiki red/solid\nThis is wiki markup in a '''div''' with __css__ `class=\"red solid\"`.\n}}}", "{{{#!wiki red/solid\nThis is wiki markup in a '''div''' with __css__ `class=\"red solid\"`.\n}}}\n"),
-            (u"{{{#!creole(class=\"par: arg para: arga\" style=\"st: er\")\n... **bold** ...\n}}}", u"{{{#!creole(style=\"st: er\" class=\"par: arg para: arga\")\n... **bold** ...\n}}}\n"),
-            (u"#format creole\n... **bold** ...\n", "#format creole\n... **bold** ...\n"),
+            # (u"{{{#!creole(class=\"par: arg para: arga\" style=\"st: er\")\n... **bold** ...\n}}}", u"{{{#!creole(style=\"st: er\" class=\"par: arg para: arga\")\n... **bold** ...\n}}}\n"),
+            # (u"#format creole\n... **bold** ...\n", "#format creole\n... **bold** ...\n"),
         ]
         for i in data:
             yield (self.do, ) + i
@@ -80,24 +80,24 @@
     def test_link(self):
         data = [
             (u'[[SomePage#subsection|subsection of Some Page]]', '[[SomePage#subsection|subsection of Some Page]]\n'),
-            (u'[[SomePage|{{attachment:samplegraphic.png}}|target=_blank]]', '[[SomePage|{{attachment:samplegraphic.png}}|target=_blank]]\n'),
-            (u'[[SomePage|{{attachment:samplegraphic.png}}|&target=_blank]]', '[[SomePage|{{attachment:samplegraphic.png}}|&target=_blank]]\n'),
+            # (u'[[SomePage|{{attachment:samplegraphic.png}}|target=_blank]]', '[[SomePage|{{attachment:samplegraphic.png}}|target=_blank]]\n'),
+            # (u'[[SomePage|{{attachment:samplegraphic.png}}|&target=_blank]]', '[[SomePage|{{attachment:samplegraphic.png}}|&target=_blank]]\n'),
             (u'[[../SisterPage|link text]]', '[[../SisterPage|link text]]\n'),
-            (u'[[http://static.moinmo.in/logos/moinmoin.png|{{attachment:samplegraphic.png}}|target=_blank]]', '[[http://static.moinmo.in/logos/moinmoin.png|{{attachment:samplegraphic.png}}|target=_blank]]\n'),
-            (u'[[http://moinmo.in/|MoinMoin Wiki|class=green dotted, accesskey=1]]', '[[http://moinmo.in/|MoinMoin Wiki|class=green dotted,accesskey=1]]\n'),
-            (u'[[MoinMoin:MoinMoinWiki|MoinMoin Wiki|&action=diff,&rev1=1,&rev2=2]]', '[[MoinMoin:MoinMoinWiki|MoinMoin Wiki|&action=diff,&rev1=1,&rev2=2]]\n'),
-            (u'[[attachment:HelpOnImages/pineapple.jpg|a pineapple|&do=get]]', '[[attachment:HelpOnImages/pineapple.jpg|a pineapple|&do=get]]\n'),
-            (u'[[attachment:filename.txt]]', '[[attachment:filename.txt]]\n')
+            # (u'[[http://static.moinmo.in/logos/moinmoin.png|{{attachment:samplegraphic.png}}|target=_blank]]', '[[http://static.moinmo.in/logos/moinmoin.png|{{attachment:samplegraphic.png}}|target=_blank]]\n'),
+            # (u'[[http://moinmo.in/|MoinMoin Wiki|class=green dotted, accesskey=1]]', '[[http://moinmo.in/|MoinMoin Wiki|class=green dotted,accesskey=1]]\n'),
+            # (u'[[MoinMoin:MoinMoinWiki|MoinMoin Wiki|&action=diff,&rev1=1,&rev2=2]]', '[[MoinMoin:MoinMoinWiki|MoinMoin Wiki|&action=diff,&rev1=1,&rev2=2]]\n'),
+            # (u'[[attachment:HelpOnImages/pineapple.jpg|a pineapple|&do=get]]', '[[attachment:HelpOnImages/pineapple.jpg|a pineapple|&do=get]]\n'),
+            # (u'[[attachment:filename.txt]]', '[[attachment:filename.txt]]\n')
         ]
         for i in data:
             yield (self.do, ) + i
 
     def test_list(self):
         data = [
-            (u" * A\n * B\n  1. C\n  1. D\n   I. E\n   I. F\n", ' * A\n * B\n  1. C\n  1. D\n   I. E\n   I. F\n'),
+            # (u" * A\n * B\n  1. C\n  1. D\n   I. E\n   I. F\n", ' * A\n * B\n  1. C\n  1. D\n   I. E\n   I. F\n'),
             (u" i. E\n i. F\n", " i. E\n i. F\n"),
             (u" A:: B\n :: C\n :: D\n", ' A::\n :: B\n :: C\n :: D\n'),
-            (u" A::\n :: B\n :: C\n :: D\n", ' A::\n :: B\n :: C\n :: D\n'),
+            # (u" A::\n :: B\n :: C\n :: D\n", ' A::\n :: B\n :: C\n :: D\n'),
         ]
         for i in data:
             yield (self.do, ) + i
@@ -115,19 +115,20 @@
 
     def test_object(self):
         data = [
-            (u"{{drawing:anywikitest.adraw}}", '{{drawing:anywikitest.adraw}}\n'),
+            # (u"{{drawing:anywikitest.adraw}}", '{{drawing:anywikitest.adraw}}\n'),
             (u"{{http://static.moinmo.in/logos/moinmoin.png}}\n", '{{http://static.moinmo.in/logos/moinmoin.png}}\n'),
             (u'{{http://static.moinmo.in/logos/moinmoin.png|alt text}}', '{{http://static.moinmo.in/logos/moinmoin.png|alt text}}\n'),
-            (u'{{http://static.moinmo.in/logos/moinmoin.png|alt text|width=100 height=150 align=right}}', '{{http://static.moinmo.in/logos/moinmoin.png|alt text|width=100 height=150 align=right}}\n'),
-            (u'{{attachment:image.png}}', '{{attachment:image.png}}\n'),
-            (u'{{attachment:image.png|alt text}}', '{{attachment:image.png|alt text}}\n'),
-            (u'{{attachment:image.png|alt text|width=100 align=left height=150}}', '{{attachment:image.png|alt text|width=100 align=left height=150}}\n'),
+            # (u'{{http://static.moinmo.in/logos/moinmoin.png|alt text|width=100 height=150 align=right}}', '{{http://static.moinmo.in/logos/moinmoin.png|alt text|width=100 height=150 align=right}}\n'),
+            # (u'{{attachment:image.png}}', '{{attachment:image.png}}\n'),
+            # (u'{{attachment:image.png|alt text}}', '{{attachment:image.png|alt text}}\n'),
+            # (u'{{attachment:image.png|alt text|width=100 align=left height=150}}', '{{attachment:image.png|alt text|width=100 align=left height=150}}\n'),
 
         ]
         for i in data:
             yield (self.do, ) + i
 
     def test_page(self):
+        pytest.skip("please help fixing moin wiki round trip tests") # XXX TODO
         data = [
             (u"""
 This page aims to introduce the most important elements of MoinMoin``'s syntax at a glance, showing first the markup verbatim and then how it is rendered by the wiki engine. Additionally, you'll find links to the relative help pages. Please note that some of the features depend on your configuration.
@@ -512,4 +513,3 @@
         out = self.conv_in(input, 'text/x.moin.wiki;charset=utf-8', **args)
         out = self.conv_out(self.handle_input(self.serialize(out)), **args)
         assert self.handle_output(out) == output
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/converter/_tests/test_registry.py	Sat Oct 06 17:46:24 2012 +0200
@@ -0,0 +1,63 @@
+# Copyright: 2012 MoinMoin:CheerXiao
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+MoinMoin - Tests for MoinMoin.converter.default_registry
+"""
+
+
+import pytest
+
+from MoinMoin.util.mime import Type, type_moin_document, type_moin_wiki
+from MoinMoin.converter import default_registry
+from MoinMoin.converter.text_in import Converter as TextInConverter
+from MoinMoin.converter.moinwiki_in import Converter as MoinwikiInConverter
+from MoinMoin.converter.html_in import Converter as HtmlInConverter
+from MoinMoin.converter.pygments_in import Converter as PygmentsInConverter
+from MoinMoin.converter.everything import Converter as EverythingConverter
+
+from MoinMoin.converter.html_out import ConverterPage as HtmlOutConverterPage
+from MoinMoin.converter.moinwiki_out import Converter as MoinwikiOutConverter
+
+from MoinMoin.converter.highlight import Converter as HighlightConverter
+from MoinMoin.converter.macro import Converter as MacroConverter
+from MoinMoin.converter.include import Converter as IncludeConverter
+from MoinMoin.converter.link import ConverterExternOutput as LinkConverterExternOutput
+from MoinMoin.converter.link import ConverterItemRefs as LinkConverterItemRefs
+
+
+class TestRegistry(object):
+    def testConverterFinder(self):
+        for type_input, type_output, ExpectedClass in [
+                # *_in converters
+                (type_moin_wiki, type_moin_document, MoinwikiInConverter),
+                (Type('x-moin/format;name=wiki'), type_moin_document, MoinwikiInConverter),
+                # pygments_in can handle this too but html_in should have more priority
+                (Type('text/html'), type_moin_document, HtmlInConverter),
+                # fall back to pygments_in
+                (Type('text/html+jinja'), type_moin_document, PygmentsInConverter),
+                # fallback for any random text/* input types
+                (Type('text/blahblah'), type_moin_document, TextInConverter),
+                # fallback for anything
+                (Type('mua/haha'), type_moin_document, EverythingConverter),
+
+                # *_out converters
+                (type_moin_document, Type('application/x-xhtml-moin-page'), HtmlOutConverterPage),
+                (type_moin_document, type_moin_wiki, MoinwikiOutConverter),
+                (type_moin_document, Type('x-moin/format;name=wiki'), MoinwikiOutConverter),
+            ]:
+            conv = default_registry.get(type_input, type_output)
+            assert isinstance(conv, ExpectedClass)
+
+        for kwargs, ExpectedClass in [
+                # DOM converters, which depend on keyword argument to default_registry.get
+                (dict(macros='expandall'), MacroConverter),
+                (dict(includes='expandall'), IncludeConverter),
+                (dict(links='extern'), LinkConverterExternOutput),
+                (dict(items='refs'), LinkConverterItemRefs),
+            ]:
+            conv = default_registry.get(type_moin_document, type_moin_document, **kwargs)
+            assert isinstance(conv, ExpectedClass)
+
+
+coverage_modules = ['MoinMoin.converter']
--- a/MoinMoin/converter/_tests/test_rst_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_tests/test_rst_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -34,9 +34,9 @@
                 '<page><body><p>Text</p><p>Test</p></body></page>'),
             (u'H\\ :sub:`2`\\ O\n\nE = mc\\ :sup:`2`', '<page><body><p>H<span baseline-shift="sub">2</span>O</p><p>E = mc<span baseline-shift="super">2</span></p></body></page>'),
             (u'| Lend us a couple of bob till Thursday.', '<page><body>Lend us a couple of bob till Thursday.</body></page>'),
-            (u'**Text**', '<page><body><p><strong>Text</strong></p></body></page>' ),
-            (u'*Text*', '<page><body><p><emphasis>Text</emphasis></p></body></page>' ),
-            (u'``Text``', '<page><body><p><code>Text</code></p></body></page>' ),
+            (u'**Text**', '<page><body><p><strong>Text</strong></p></body></page>'),
+            (u'*Text*', '<page><body><p><emphasis>Text</emphasis></p></body></page>'),
+            (u'``Text``', '<page><body><p><code>Text</code></p></body></page>'),
             (u"`Text <javascript:alert('xss')>`_", u'<page><body><p>Text</p></body></page>'),
         ]
         for i in data:
--- a/MoinMoin/converter/_util.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_util.py	Sat Oct 06 17:46:24 2012 +0200
@@ -8,9 +8,14 @@
 
 from __future__ import absolute_import, division
 
+from flask import request
+from flask import g as flaskg
+from emeraldtree import ElementTree as ET
+
 from MoinMoin.config import uri_schemes
 from MoinMoin.util.iri import Iri
 from MoinMoin.util.mime import Type
+from MoinMoin.util.tree import html, moin_page
 
 def allowed_uri_scheme(uri):
     parsed = Iri(uri)
@@ -49,3 +54,109 @@
     lines = text.split(u'\n')
     return lines
 
+
+class _Iter(object):
+    """
+    Iterator with push back support
+
+    Collected items can be pushed back into the iterator and further calls will
+    return them.
+
+    Increments a counter tracking the current line number. This is used by _Stack to
+    add an attribute used by javascript to autoscroll the edit textarea.
+    """
+
+    def __init__(self, parent, startno=0):
+        self.__finished = False
+        self.__parent = iter(parent)
+        self.__prepend = []
+        self.lineno = startno
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        if self.__finished:
+            raise StopIteration
+
+        self.lineno += 1
+        if self.__prepend:
+            return self.__prepend.pop(0)
+
+        try:
+            return self.__parent.next()
+        except StopIteration:
+            self.__finished = True
+            raise
+
+    def push(self, item):
+        self.__prepend.append(item)
+        self.lineno -= 1
+
+
+class _Stack(object):
+    class Item(object):
+        def __init__(self, elem):
+            self.elem = elem
+            if elem.tag.uri == moin_page:
+                self.name = elem.tag.name
+            else:
+                self.name = None
+
+    def __init__(self, bottom=None, iter_content=None):
+        self._list = []
+        if bottom:
+            self._list.append(self.Item(bottom))
+        self.iter_content = iter_content
+        self.last_lineno = 0
+
+    def __len__(self):
+        return len(self._list)
+
+    def add_lineno(self, elem):
+        """
+        Add a custom attribute (data-lineno=nn) that will be used by Javascript to scroll edit textarea.
+        """
+        if request.user_agent and flaskg.user.edit_on_doubleclick:
+            # this is not py.test and user has option to edit on doubleclick
+            # TODO: move the 2 lines above and 2 related import statements outside of the converters (needed for standalone converter)
+            if self.last_lineno != self.iter_content.lineno:
+                # avoid adding same lineno to parent and multiple children or grand-children
+                elem.attrib[html.data_lineno] = self.iter_content.lineno
+                self.last_lineno = self.iter_content.lineno
+
+    def clear(self):
+        del self._list[1:]
+
+    def pop(self):
+        self._list.pop()
+
+    def pop_name(self, *names):
+        """
+        Remove anything from the stack including the given node.
+        """
+        while len(self._list) > 2 and not self.top_check(*names):
+            self.pop()
+        self.pop()
+
+    def push(self, elem):
+        self.top_append(elem)
+        self._list.append(self.Item(elem))
+
+    def top(self):
+        return self._list[-1].elem
+
+    def top_append(self, elem):
+        if isinstance(elem, ET.Node):
+            self.add_lineno(elem)
+        self.top().append(elem)
+
+    def top_append_ifnotempty(self, elem):
+        if elem:
+            self.top_append(elem)
+
+    def top_check(self, *names):
+        """
+        Checks if the name of the top of the stack matches the parameters.
+        """
+        return self._list[-1].name in names
--- a/MoinMoin/converter/_wiki_macro.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/_wiki_macro.py	Sat Oct 06 17:46:24 2012 +0200
@@ -7,6 +7,8 @@
 Base class for wiki parser with macro support.
 """
 
+from MoinMoin import log
+logging = log.getLogger(__name__)
 
 from emeraldtree import ElementTree as ET
 
@@ -22,13 +24,13 @@
 
     def _FootNote_repl(self, args, text, context_block):
         if not args:
-            # TODO: footnote placing
-            return
+            # return a minimal note elem to indicate explicit footnote placement
+            elem = moin_page.note()
+            return elem
 
         text = self.macro_text(' '.join(args.positional))
         elem_body = moin_page.note_body(children=text)
         attrib = {moin_page.note_class: 'footnote'}
-
         elem = moin_page.note(attrib=attrib, children=[elem_body])
 
         if context_block:
@@ -110,8 +112,10 @@
     def macro(self, name, args, text, context_block=False):
         func = getattr(self, '_{0}_repl'.format(name), None)
         if func is not None:
+            logging.debug("builtin macro: %r" % name)
             return func(args, text, context_block)
 
+        logging.debug("extension macro: %r" % name)
         tag = context_block and moin_page.part or moin_page.inline_part
 
         elem = tag(attrib={
@@ -146,6 +150,7 @@
             type = Type(name)
         else:
             type = Type(type='x-moin', subtype='format', parameters={'name': name})
+        logging.debug("parser type: %r" % type)
 
         elem = moin_page.part(attrib={moin_page.content_type: type})
 
@@ -164,5 +169,3 @@
             elem.append(moin_page.body(children=content))
 
         return elem
-
-
--- a/MoinMoin/converter/archive_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/archive_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -38,7 +38,7 @@
 
     def process_name(self, member_name):
         attrib = {
-            xlink.href: Iri(scheme='wiki', authority='', path='/'+self.item_name, query='do=get&member={0}'.format(member_name)),
+            xlink.href: Iri(scheme='wiki', authority='', path='/'+self.item_name, query=u'do=get&member={0}'.format(member_name)),
         }
         return moin_page.a(attrib=attrib, children=[member_name, ])
 
@@ -56,7 +56,9 @@
                          self.process_datetime(dt),
                          self.process_name(name),
                         ) for size, dt, name in contents]
-            return self.build_dom_table(contents, head=[_("Size"), _("Date"), _("Name")], cls='zebra')
+            table = self.build_dom_table(contents, head=[_("Size"), _("Date"), _("Name")], cls='zebra')
+            body = moin_page.body(children=(table, ))
+            return moin_page.page(children=(body, ))
         except ArchiveException as err:
             logging.exception("An exception within archive file handling occurred:")
             # XXX we also use a table for error reporting, could be
@@ -122,4 +124,3 @@
 default_registry.register(TarConverter._factory, Type('application/x-tar'), type_moin_document)
 default_registry.register(TarConverter._factory, Type('application/x-gtar'), type_moin_document)
 default_registry.register(ZipConverter._factory, Type('application/zip'), type_moin_document)
-
--- a/MoinMoin/converter/audio_video_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/audio_video_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -33,7 +33,9 @@
             moin_page.type_: unicode(self.input_type),
             xlink.href: Iri(scheme='wiki', authority='', path='/'+item_name, query='do=get&rev={0}'.format(rev.revid)),
         }
-        return moin_page.object_(attrib=attrib, children=[u'Your Browser does not support HTML5 audio/video element.', ])
+        obj = moin_page.object_(attrib=attrib, children=[u'Your Browser does not support HTML5 audio/video element.', ])
+        body = moin_page.body(children=(obj, ))
+        return moin_page.page(children=(body, ))
 
 
 from . import default_registry
@@ -42,4 +44,3 @@
 default_registry.register(Converter._factory, Type('video/webm'), type_moin_document)
 default_registry.register(Converter._factory, Type('audio/ogg'), type_moin_document)
 default_registry.register(Converter._factory, Type('audio/wave'), type_moin_document)
-
--- a/MoinMoin/converter/creole_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/creole_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -33,73 +33,8 @@
 
 from ._args_wiki import parse as parse_arguments
 from ._wiki_macro import ConverterMacro
-from ._util import decode_data, normalize_split_text
-
-
-class _Iter(object):
-    """
-    Iterator with push back support
-
-    Collected items can be pushed back into the iterator and further calls will
-    return them.
-    """
-
-    def __init__(self, parent):
-        self.__finished = False
-        self.__parent = iter(parent)
-        self.__prepend = []
-
-    def __iter__(self):
-        return self
-
-    def next(self):
-        if self.__finished:
-            raise StopIteration
-
-        if self.__prepend:
-            return self.__prepend.pop(0)
-
-        try:
-            return self.__parent.next()
-        except StopIteration:
-            self.__finished = True
-            raise
+from ._util import decode_data, normalize_split_text, _Iter, _Stack
 
-    def push(self, item):
-        self.__prepend.append(item)
-
-class _Stack(list):
-    def clear(self):
-        del self[1:]
-
-    def pop_name(self, *names):
-        """
-        Remove anything from the stack including the given node.
-        """
-        while len(self) > 2 and not self.top_check(*names):
-            self.pop()
-        self.pop()
-
-    def push(self, elem):
-        self.top_append(elem)
-        self.append(elem)
-
-    def top(self):
-        return self[-1]
-
-    def top_append(self, elem):
-        self[-1].append(elem)
-
-    def top_append_ifnotempty(self, elem):
-        if elem:
-            self.top_append(elem)
-
-    def top_check(self, *names):
-        """
-        Checks if the name of the top of the stack matches the parameters.
-        """
-        tag = self[-1].tag
-        return tag.uri == moin_page.namespace and tag.name in names
 
 class Converter(ConverterMacro):
     @classmethod
@@ -259,7 +194,7 @@
             stack.push(moin_page.blockcode())
             return
 
-        lines = _Iter(self.block_nowiki_lines(iter_content))
+        lines = _Iter(self.block_nowiki_lines(iter_content), startno=iter_content.lineno)
 
         match = self.nowiki_interpret_re.match(firstline)
 
@@ -679,7 +614,7 @@
 
         body = moin_page.body(attrib=attrib)
 
-        stack = _Stack([body])
+        stack = _Stack(body, iter_content=iter_content)
 
         # Please note that the iterator can be modified by other functions
         for line in iter_content:
@@ -706,4 +641,3 @@
 from MoinMoin.util.mime import Type, type_moin_document, type_moin_creole
 default_registry.register(Converter.factory, type_moin_creole, type_moin_document)
 default_registry.register(Converter.factory, Type('x-moin/format;name=creole'), type_moin_document)
-
--- a/MoinMoin/converter/docbook_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/docbook_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -43,7 +43,7 @@
 
     # DocBook elements which are completely ignored by our converter
     # We even do not process children of these elements
-    ignored_tags = set([#Info elements
+    ignored_tags = set([# Info elements
                        'abstract', 'artpagenums', 'annotation',
                        'artpagenums', 'author', 'authorgroup',
                        'authorinitials', 'bibliocoverage', 'biblioid',
@@ -1154,4 +1154,3 @@
 from . import default_registry
 from MoinMoin.util.mime import Type, type_moin_document
 default_registry.register(Converter._factory, Type('application/docbook+xml'), type_moin_document)
-
--- a/MoinMoin/converter/docbook_out.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/docbook_out.py	Sat Oct 06 17:46:24 2012 +0200
@@ -256,7 +256,7 @@
 
         # We will close a section before starting a new one
         # Need more test
-        elif  depth < self.current_section:
+        elif depth < self.current_section:
             if self.parent_section != 0:
                 section_tag = 'sect{0}'.format(self.parent_section)
                 section = ET.Element(docbook(section_tag), attrib={},
@@ -356,7 +356,7 @@
         mimetype = Type(_type=element.get(moin_page.type_, 'application/x-nonexistent'))
         if href:
             attrib[docbook.fileref] = href
-            if  Type('image/').issupertype(mimetype):
+            if Type('image/').issupertype(mimetype):
                 object_data = self.new(docbook.imagedata, attrib=attrib,
                                        children=[])
                 object_element = self.new(docbook.imageobject, attrib={},
@@ -378,7 +378,6 @@
         return self.new(docbook.inlinemediaobject, attrib={},
                         children=[object_element])
 
-
     def visit_moinpage_table(self, element):
         # TODO: Attributes conversion
         title = element.get(html('title'))
@@ -517,4 +516,3 @@
 from MoinMoin.util.mime import Type, type_moin_document
 default_registry.register(Converter._factory, type_moin_document,
     Type('application/docbook+xml'))
-
--- a/MoinMoin/converter/everything.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/everything.py	Sat Oct 06 17:46:24 2012 +0200
@@ -27,13 +27,12 @@
         attrib = {
             xlink.href: Iri(scheme='wiki', authority='', path='/'+item_name, query='do=get&rev={0}'.format(rev.revid)),
         }
-        return moin_page.a(attrib=attrib, children=["Download {0}.".format(item_name)])
+        a = moin_page.a(attrib=attrib, children=[u"Download {0}.".format(item_name)])
+        body = moin_page.body(children=(a, ))
+        return moin_page.page(children=(body, ))
 
 
 from . import default_registry
 from MoinMoin.util.mime import Type, type_moin_document
-default_registry.register(Converter._factory, Type('application/octet-stream'), type_moin_document,
-                          default_registry.PRIORITY_MIDDLE + 3)
-default_registry.register(Converter._factory, Type(type=None, subtype=None), type_moin_document,
-                          default_registry.PRIORITY_MIDDLE + 3)
-
+default_registry.register(Converter._factory, Type('application/octet-stream'), type_moin_document)
+default_registry.register(Converter._factory, Type(type=None, subtype=None), type_moin_document)
--- a/MoinMoin/converter/highlight.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/highlight.py	Sat Oct 06 17:46:24 2012 +0200
@@ -13,10 +13,9 @@
 
 class Converter(object):
     @classmethod
-    def _factory(cls, input, output, **kw):
-        if input == 'application/x.moin.document' and \
-                output == 'application/x.moin.document;highlight=regex':
-            return cls
+    def _factory(cls, input, output, highlight='', re='', **kw):
+        if highlight == 'regex':
+            return cls(re)
 
     def recurse(self, elem):
         new_childs = []
@@ -57,4 +56,3 @@
 from . import default_registry
 from MoinMoin.util.mime import Type, type_moin_document
 default_registry.register(Converter._factory, type_moin_document, type_moin_document)
-
--- a/MoinMoin/converter/html_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/html_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -559,4 +559,3 @@
 from . import default_registry
 from MoinMoin.util.mime import Type, type_moin_document
 default_registry.register(Converter._factory, Type('text/html'), type_moin_document)
-
--- a/MoinMoin/converter/html_out.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/html_out.py	Sat Oct 06 17:46:24 2012 +0200
@@ -14,7 +14,6 @@
 
 import re
 
-from flask import request
 from emeraldtree import ElementTree as ET
 
 from MoinMoin import wikiutil
@@ -25,39 +24,6 @@
 logging = log.getLogger(__name__)
 
 
-def convert_getlink_to_showlink(href):
-    """
-    If the incoming transclusion reference is within this domain, then remove "+get/<revision number>/".
-    """
-    if href.startswith('/'):
-        return re.sub(r'\+get/\+[0-9a-fA-F]+/', '', href)
-    return href
-
-def mark_item_as_transclusion(elem, href):
-    """
-    Return elem after adding a "moin-transclusion" class and a "data-href" attribute with
-    a link to the transcluded item.
-
-    On the client side, a Javascript function will wrap the element (or a parent element)
-    in a span or div and 2 overlay siblings will be created.
-    """
-    href = unicode(href)
-    # href will be "/wikiroot/SomeObject" or "/SomePage" for internal wiki items
-    # or "http://Some.Org/SomeThing" for external link
-    if elem.tag.name == 'page':
-        # if wiki is not running at server root, prefix href with wiki root
-        wiki_root = request.url_root[len(request.host_url):-1]
-        if wiki_root:
-            href = '/' + wiki_root + href
-    href = convert_getlink_to_showlink(href)
-    # data_href will create an attribute named data-href: any attribute beginning with "data-" passes html5 validation
-    elem.attrib[html.data_href] = href
-    classes = elem.attrib.get(html.class_, '').split()
-    classes.append('moin-transclusion')
-    elem.attrib[html.class_] = ' '.join(classes)
-    return elem
-
-
 class ElementException(RuntimeError):
     pass
 
@@ -221,7 +187,7 @@
         return pre
 
     def visit_moinpage_blockquote(self, elem):
-        return  self.new_copy(html.blockquote, elem)
+        return self.new_copy(html.blockquote, elem)
 
     def visit_moinpage_code(self, elem):
         return self.new_copy(html.code, elem)
@@ -367,7 +333,7 @@
 
         if obj_type == "img":
             # Images have alt text
-            alt = ''.join(str(e) for e in elem) # XXX handle non-text e
+            alt = ''.join(unicode(e) for e in elem) # XXX handle non-text e
             if alt:
                 attrib[html.alt] = alt
             new_elem = html.img(attrib=attrib)
@@ -378,7 +344,7 @@
                 attrib[html.controls] = 'controls'
             new_elem = self.new_copy(getattr(html, obj_type), elem, attrib)
 
-        return mark_item_as_transclusion(new_elem, href)
+        return new_elem
 
     def visit_moinpage_p(self, elem):
         return self.new_copy(html.p, elem)
@@ -506,6 +472,12 @@
         nr = self._ids[id] = self._ids.get(id, 0) + 1
         return nr
 
+    def zero_id(self, id):
+        self._ids[id] = 0
+
+    def get_id(self, id):
+        return self._ids.get(id, 0)
+
     def gen_text(self, text):
         id = wikiutil.anchor_name_from_text(text)
         nr = self._ids[id] = self._ids.get(id, 0) + 1
@@ -523,10 +495,13 @@
     def add_footnote(self, elem):
         self._footnotes.append(elem)
 
+    def remove_footnotes(self):
+        self._footnotes = []
+
     def add_heading(self, elem, level, id=None):
         elem.append(html.a(attrib={
             html.href: "#{0}".format(id),
-            html.class_: "permalink",
+            html.class_: "moin-permalink",
             html.title_: _("Link to this heading")
         }, children=(u"ΒΆ", )))
         self._headings.append((elem, level, id))
@@ -579,10 +554,8 @@
 
         for special in self._special:
             if special._footnotes:
-                footnotes_div = html.div({html.class_: "moin-footnotes"})
+                footnotes_div = self.create_footnotes(special)
                 special.root.append(footnotes_div)
-                for elem in special.footnotes():
-                    footnotes_div.append(elem)
 
             for elem, headings in special.tocs():
                 headings = list(headings)
@@ -629,7 +602,7 @@
                                          html.href_: "#",
                                          html.onclick_:
                                             "$('#li{0} ol').toggle();return false;".format(id),
-                                         html.class_: 'showhide',
+                                         html.class_: 'moin-showhide',
                                      },
                                      children=["[+]", ])
                     elem_a = html.a(attrib={html.href: '#' + id},
@@ -666,8 +639,27 @@
         self._special_stack[-1].add_heading(elem, elem.level, id)
         return elem
 
+    def create_footnotes(self, top):
+        """Return footnotes formatted into an ET structure."""
+        footnotes_div = html.div({html.class_: "moin-footnotes"})
+        for elem in top.footnotes():
+            footnotes_div.append(elem)
+        return footnotes_div
+
     def visit_moinpage_note(self, elem):
         # TODO: Check note-class
+        top = self._special_stack[-1]
+        if len(elem) == 0:
+            # explicit footnote placement:  show prior footnotes, empty stack, reset counter
+            if len(top._footnotes) == 0:
+                return
+
+            footnotes_div = self.create_footnotes(top)
+            top.remove_footnotes()
+            self._id.zero_id('note')
+            # bump note-placement counter to insure unique footnote ids
+            self._id.gen_id('note-placement')
+            return footnotes_div
 
         body = None
         for child in elem:
@@ -676,25 +668,27 @@
                     body = self.do_children(child)
 
         id = self._id.gen_id('note')
+        prefixed_id = '%s-%s' % (self._id.get_id('note-placement'), id)
 
         elem_ref = ET.XML("""
 <html:sup xmlns:html="{0}" html:id="note-{1}-ref" html:class="moin-footnote"><html:a html:href="#note-{2}">{3}</html:a></html:sup>
-""".format(html, id, id, id))
+""".format(html, prefixed_id, prefixed_id, id))
 
         elem_note = ET.XML("""
 <html:p xmlns:html="{0}" html:id="note-{1}"><html:sup><html:a html:href="#note-{2}-ref">{3}</html:a></html:sup></html:p>
-""".format(html, id, id, id))
+""".format(html, prefixed_id, prefixed_id, id))
 
         elem_note.extend(body)
-        self._special_stack[-1].add_footnote(elem_note)
+        top.add_footnote(elem_note)
 
         return elem_ref
 
     def visit_moinpage_table_of_content(self, elem):
         level = int(elem.get(moin_page.outline_level, 6))
 
-        attrib = {html.class_: 'moin-table-of-contents'}
-        elem = html.div(attrib=attrib)
+        attribs = elem.attrib.copy()
+        attribs[html.class_] = 'moin-table-of-contents'
+        elem = html.div(attrib=attribs)
 
         self._special_stack[-1].add_toc(elem, level)
         return elem
@@ -709,4 +703,3 @@
 from . import default_registry
 from MoinMoin.util.mime import Type, type_moin_document
 default_registry.register(ConverterPage._factory, type_moin_document, Type('application/x-xhtml-moin-page'))
-
--- a/MoinMoin/converter/image_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/image_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -31,7 +31,9 @@
             moin_page.type_: unicode(self.input_type),
             xlink.href: Iri(scheme='wiki', authority='', path='/'+item_name, query='do=get&rev={0}'.format(rev.revid)),
         }
-        return moin_page.object_(attrib=attrib, children=[item_name, ])
+        obj = moin_page.object_(attrib=attrib, children=[item_name, ])
+        body = moin_page.body(children=(obj, ))
+        return moin_page.page(children=(body, ))
 
 
 from . import default_registry
@@ -40,4 +42,3 @@
 default_registry.register(Converter._factory, Type('image/png'), type_moin_document)
 default_registry.register(Converter._factory, Type('image/jpeg'), type_moin_document)
 default_registry.register(Converter._factory, Type('image/gif'), type_moin_document)
-
--- a/MoinMoin/converter/include.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/include.py	Sat Oct 06 17:46:24 2012 +0200
@@ -20,6 +20,7 @@
 from MoinMoin import log
 logging = log.getLogger(__name__)
 
+from flask import request
 from flask import current_app as app
 from flask import g as flaskg
 
@@ -31,7 +32,41 @@
 from MoinMoin.util.iri import Iri, IriPath
 from MoinMoin.util.tree import html, moin_page, xinclude, xlink
 
-from MoinMoin.converter.html_out import mark_item_as_transclusion, Attributes
+from MoinMoin.converter.html_out import Attributes
+
+
+def convert_getlink_to_showlink(href):
+    """
+    If the incoming transclusion reference is within this domain, then remove "+get/<revision number>/".
+    """
+    if href.startswith('/'):
+        return re.sub(r'\+get/\+[0-9a-fA-F]+/', '', href)
+    return href
+
+def mark_item_as_transclusion(elem, href):
+    """
+    Return elem after adding a "moin-transclusion" class and a "data-href" attribute with
+    a link to the transcluded item.
+
+    On the client side, a Javascript function will wrap the element (or a parent element)
+    in a span or div and 2 overlay siblings will be created.
+    """
+    href = unicode(href)
+    # href will be "/wikiroot/SomeObject" or "/SomePage" for internal wiki items
+    # or "http://Some.Org/SomeThing" for external link
+    if elem.tag.name not in ('object', 'img'):
+        # XXX see issue #167: for wikis not running at root, only object and img elements have complete path
+        # if wiki is not running at server root, prefix href with wiki root
+        wiki_root = request.url_root[len(request.host_url):-1]
+        if wiki_root:
+            href = '/' + wiki_root + href
+    href = convert_getlink_to_showlink(href)
+    # data_href will create an attribute named data-href: any attribute beginning with "data-" passes html5 validation
+    elem.attrib[html.data_href] = href
+    classes = elem.attrib.get(html.class_, '').split()
+    classes.append('moin-transclusion')
+    elem.attrib[html.class_] = ' '.join(classes)
+    return elem
 
 
 class XPointer(list):
@@ -233,8 +268,8 @@
                         loop = self.stack[self.stack.index(p_href):]
                         loop = [u'{0}'.format(ref.path[1:]) for ref in loop if ref is not None] + [page.name]
                         msg = u'Error: Transclusion loop via: ' + u', '.join(loop)
-                        attrib = {getattr(html, 'class'): 'moin-error'}
-                        strong = ET.Element(html.strong, attrib, (msg, ))
+                        attrib = {getattr(moin_page, 'class'): 'moin-error'}
+                        strong = ET.Element(moin_page.strong, attrib, (msg, ))
                         included_elements.append(strong)
                         continue
                     # TODO: Is this correct?
@@ -249,15 +284,13 @@
                         elem_h = ET.Element(self.tag_h, attrib, children=(elem_a, ))
                         included_elements.append(elem_h)
 
-                    page_doc = page.internal_representation()
+                    page_doc = page.content.internal_representation()
                     # page_doc.tag = self.tag_div # XXX why did we have this?
 
                     self.recurse(page_doc, page_href)
 
-                    # if this is an existing item, mark it as a transclusion.  non-existent items are not marked (page_doc.tag.name == u'a')
                     # The href needs to be an absolute URI, without the prefix "wiki://"
-                    if page_doc.tag.name == u'page':
-                        page_doc = mark_item_as_transclusion(page_doc, p_href.path)
+                    page_doc = mark_item_as_transclusion(page_doc, p_href.path)
                     included_elements.append(page_doc)
 
                 if len(included_elements) > 1:
@@ -271,7 +304,6 @@
                 #  end of processing for transclusion; the "result" will get inserted into the DOM below
                 return result
 
-
             # Traverse the DOM by calling self.recurse with each child of the current elem.  Starting elem.tag.name=='page'.
             container = []
             i = 0
@@ -302,7 +334,7 @@
                                 # get attributes from page node; we expect {class: "moin-transclusion"; data-href: "http://some.org/somepage"}
                                 attrib = Attributes(ret).convert()
                                 # make new span node and "convert" p to span by copying all of p's children
-                                span = ET.Element(html.span, attrib=attrib, children=p[:])
+                                span = ET.Element(moin_page.span, attrib=attrib, children=p[:])
                                 # insert the new span into the DOM replacing old include, page, body, and p elements
                                 elem[i] = span
                             elif not isinstance(body, unicode) and ret.tag.name == 'page' and body.tag.name == 'body':
@@ -310,19 +342,19 @@
                                 # note: ancestor P may have text before or after include
                                 if i > 0:
                                     # there is text before transclude, make new p node to hold text before include and save in container
-                                    pa = ET.Element(html.p)
+                                    pa = ET.Element(moin_page.p)
                                     pa[:] = elem[0:i]
                                     container.append(pa)
                                 # get attributes from page node; we expect {class: "moin-transclusion"; data-href: "http://some.org/somepage"}
                                 attrib = Attributes(ret).convert()
                                 # make new div node, copy all of body's children, and save in container
-                                div = ET.Element(html.div, attrib=attrib, children=body[:])
+                                div = ET.Element(moin_page.div, attrib=attrib, children=body[:])
                                 container.append(div)
                                  # empty elem of siblings that were just placed in container
                                 elem[0:i+1] = []
                                 if len(elem) > 0:
                                     # there is text after transclude, make new p node to hold text, copy siblings, save in container
-                                    pa = ET.Element(html.p)
+                                    pa = ET.Element(moin_page.p)
                                     pa[:] = elem[:]
                                     container.append(pa)
                                     elem[:] = []
@@ -350,4 +382,3 @@
 from . import default_registry
 from MoinMoin.util.mime import Type, type_moin_document
 default_registry.register(Converter._factory, type_moin_document, type_moin_document)
-
--- a/MoinMoin/converter/link.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/link.py	Sat Oct 06 17:46:24 2012 +0200
@@ -16,7 +16,7 @@
 from MoinMoin.util.interwiki import is_known_wiki, url_for_item
 from MoinMoin.util.iri import Iri, IriPath
 from MoinMoin.util.mime import Type, type_moin_document
-from MoinMoin.util.tree import html, moin_page, xlink, xinclude
+from MoinMoin.util.tree import moin_page, xlink, xinclude
 from MoinMoin.wikiutil import AbsItemName
 
 
@@ -36,6 +36,9 @@
     def handle_wikilocal_transclusions(self, elem, link, page_name):
         pass
 
+    def handle_external_links(self, elem, link):
+        pass
+
     def __call__(self, *args, **kw):
         """
         Calls the self.traverse_tree method
@@ -63,7 +66,7 @@
             elif xlink_href.scheme == 'wiki':
                 self.handle_wiki_links(elem, xlink_href)
             elif xlink_href.scheme:
-                elem.set(html.class_, 'moin-' + xlink_href.scheme)
+                self.handle_external_links(elem, xlink_href)
 
         elif xinclude_href:
             xinclude_href = Iri(xinclude_href)
@@ -152,7 +155,7 @@
             wn = unicode(input.authority.host)
             if is_known_wiki(wn):
                 # interwiki link
-                elem.set(html.class_, 'moin-interwiki')
+                elem.set(moin_page.class_, 'moin-interwiki')
                 wiki_name = wn
         item_name = unicode(input.path[1:])
         endpoint, rev, query = self._get_do_rev(input.query)
@@ -167,7 +170,7 @@
             path = self.absolute_path(path, page.path)
             item_name = unicode(path)
             if not flaskg.storage.has_item(item_name):
-                elem.set(html.class_, 'moin-nonexistent')
+                elem.set(moin_page.class_, 'moin-nonexistent')
         else:
             item_name = unicode(page.path[1:])
         endpoint, rev, query = self._get_do_rev(input.query)
@@ -175,6 +178,10 @@
         link = Iri(url, query=query, fragment=input.fragment)
         elem.set(self._tag_xlink_href, link)
 
+    def handle_external_links(self, elem, input):
+        elem.set(self._tag_xlink_href, input)
+        elem.set(moin_page.class_, 'moin-' + input.scheme)
+
 
 class ConverterItemRefs(ConverterBase):
     """
@@ -189,6 +196,7 @@
         super(ConverterItemRefs, self).__init__(**kw)
         self.links = set()
         self.transclusions = set()
+        self.external_links = set()
 
     def __call__(self, *args, **kw):
         """
@@ -198,6 +206,7 @@
         # in the handle methods
         self.links = set()
         self.transclusions = set()
+        self.external_links = set()
 
         super(ConverterItemRefs, self).__call__(*args, **kw)
 
@@ -229,6 +238,14 @@
         path = self.absolute_path(path, page.path)
         self.transclusions.add(unicode(path))
 
+    def handle_external_links(self, elem, input):
+        """
+        Adds the link item from the input param to self.external_links
+        :param elem: the element of the link
+        :param input: the iri of the link
+        """
+        self.external_links.add(unicode(input))
+
     def get_links(self):
         """
         return a list of unicode link target item names
@@ -241,8 +258,12 @@
         """
         return list(self.transclusions)
 
+    def get_external_links(self):
+        """
+        return a list of unicode external links target item names
+        """
+        return list(self.external_links)
 
 from . import default_registry
 default_registry.register(ConverterExternOutput._factory, type_moin_document, type_moin_document)
 default_registry.register(ConverterItemRefs._factory, type_moin_document, type_moin_document)
-
--- a/MoinMoin/converter/macro.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/macro.py	Sat Oct 06 17:46:24 2012 +0200
@@ -13,8 +13,9 @@
 from flask import current_app as app
 
 from emeraldtree import ElementTree as ET
-import logging
-logger = logging.getLogger(__name__)
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
 
 from MoinMoin.util import plugins
 from MoinMoin.i18n import _, L_, N_
@@ -22,6 +23,7 @@
 from MoinMoin.util import iri
 from MoinMoin.util.mime import type_moin_document, Type
 from MoinMoin.util.tree import html, moin_page
+from MoinMoin.util.plugins import PluginMissingError
 
 
 class Converter(object):
@@ -31,6 +33,7 @@
             return cls()
 
     def handle_macro(self, elem, page):
+        logging.debug("handle_macro elem: %r" % elem)
         type = elem.get(moin_page.content_type)
         alt = elem.get(moin_page.alt)
 
@@ -39,6 +42,7 @@
 
         type = Type(type)
         if not (type.type == 'x-moin' and type.subtype == 'macro'):
+            logging.debug("not a macro, skipping: %r" % type)
             return
 
         name = type.parameters['name']
@@ -67,18 +71,21 @@
         elem_body = context_block and moin_page.body() or moin_page.inline_body()
         elem_error = moin_page.error()
 
-        cls = plugins.importPlugin(app.cfg, 'macro', name, function='Macro')
-
         try:
+            cls = plugins.importPlugin(app.cfg, 'macro', name, function='Macro')
             macro = cls()
             ret = macro((), args, page, alt, context_block)
             elem_body.append(ret)
+
+        except PluginMissingError:
+            elem_error.append('<<%s>> %s' % (name, _('Error: invalid macro name.')))
+
         except Exception as e:
             # we do not want that a faulty macro aborts rendering of the page
             # and makes the wiki UI unusable (by emitting a Server Error),
             # thus, in case of exceptions, we just log the problem and return
             # some standard text.
-            logger.exception("Macro {0} raised an exception:".format(name))
+            logging.exception("Macro {0} raised an exception:".format(name))
             elem_error.append(_('<<%(macro_name)s: execution failed [%(error_msg)s] (see also the log)>>',
                     macro_name=name,
                     error_msg=unicode(e),
@@ -105,10 +112,8 @@
     def __call__(self, tree):
         for elem, page in self.recurse(tree, None):
             self.handle_macro(elem, page)
-
         return tree
 
 from . import default_registry
 from MoinMoin.util.mime import Type, type_moin_document
 default_registry.register(Converter._factory, type_moin_document, type_moin_document)
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/converter/markdown_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -0,0 +1,413 @@
+# Copyright: 2008-2010 MoinMoin:BastianBlank
+# Copyright: 2012 MoinMoin:AndreasKloeckner
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+MoinMoin - Markdown input converter
+
+http://daringfireball.net/projects/markdown/
+"""
+
+
+from __future__ import absolute_import, division
+
+import re
+import htmlentitydefs
+
+from MoinMoin.util.tree import moin_page, xml, html, xlink
+from ._util import allowed_uri_scheme, decode_data
+
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
+from emeraldtree import ElementTree as ET
+
+from markdown import Markdown
+import markdown.util as md_util
+
+def postproc_text(markdown, text):
+    """
+    Removes HTML or XML character references and entities from a text string.
+
+    @param text The HTML (or XML) source text.
+    @return The plain text, as a Unicode string, if necessary.
+    """
+
+    # http://effbot.org/zone/re-sub.htm#unescape-html
+
+    if text is None:
+        return None
+
+    for pp in markdown.postprocessors.values():
+        text = pp.run(text)
+
+    def fixup(m):
+        text = m.group(0)
+        if text[:2] == "&#":
+            # character reference
+            try:
+                if text[:3] == "&#x":
+                    return unichr(int(text[3:-1], 16))
+                else:
+                    return unichr(int(text[2:-1]))
+            except ValueError:
+                pass
+        else:
+            # named entity
+            try:
+                text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
+            except KeyError:
+                pass
+        return text # leave as is
+
+    return re.sub("&#?\w+;", fixup, text)
+
+class Converter(object):
+    # {{{ html conversion
+
+    # HTML tags which can be converted directly to the moin_page namespace
+    symmetric_tags = set(['div', 'p', 'strong', 'code', 'quote', 'blockquote'])
+
+    # HTML tags to define a list, except dl which is a little bit different
+    list_tags = set(['ul', 'dir', 'ol'])
+
+    # HTML tags which can be convert without attributes in a different DOM tag
+    simple_tags = {# Emphasis
+                   'em': moin_page.emphasis, 'i': moin_page.emphasis,
+                   # Strong
+                   'b': moin_page.strong, 'strong': moin_page.strong,
+                   # Code and Blockcode
+                   'pre': moin_page.blockcode, 'tt': moin_page.code,
+                   'samp': moin_page.code,
+                   # Lists
+                   'dt': moin_page.list_item_label, 'dd': moin_page.list_item_body,
+                   # TODO : Some tags related to tables can be also simplify
+                  }
+
+    # HTML Tag which does not have equivalence in the DOM Tree
+    # But we keep the information using <span element>
+    inline_tags = set(['abbr', 'acronym', 'address', 'dfn', 'kbd'])
+
+    # HTML tags which are completely ignored by our converter.
+    # We even do not process children of these elements.
+    ignored_tags = set(['applet', 'area', 'button', 'caption', 'center', 'fieldset',
+                        'form', 'frame', 'frameset', 'head', 'iframe', 'input', 'isindex',
+                        'label', 'legend', 'link', 'map', 'menu', 'noframes', 'noscript',
+                        'optgroup', 'option', 'param', 'script', 'select', 'style',
+                        'textarea', 'title', 'var',
+                       ])
+
+    # standard_attributes are html attributes which are used
+    # directly in the DOM tree, without any conversion
+    standard_attributes = set(['title', 'class', 'style'])
+
+    # Regular expression to detect an html heading tag
+    heading_re = re.compile('h[1-6]')
+
+    def new(self, tag, attrib, children):
+        """
+        Return a new element for the DOM Tree
+        """
+        return ET.Element(tag, attrib=attrib, children=children)
+
+    def new_copy(self, tag, element, attrib):
+        """
+        Function to copy one element to the DOM Tree.
+
+        It first converts the child of the element,
+        and the element itself.
+        """
+        attrib_new = self.convert_attributes(element)
+        attrib.update(attrib_new)
+        children = self.do_children(element)
+        return self.new(tag, attrib, children)
+
+    def new_copy_symmetric(self, element, attrib):
+        """
+        Create a new QName, with the same tag of the element,
+        but with a different namespace.
+
+        Then, we handle the copy normally.
+        """
+        tag = ET.QName(element.tag, moin_page)
+        return self.new_copy(tag, element, attrib)
+
+    def convert_attributes(self, element):
+        result = {}
+        for key, value in element.attrib.iteritems():
+            if key in self.standard_attributes:
+                result[html(key)] = value
+            if key == 'id':
+                result[xml('id')] = value
+        return result
+
+    def visit_heading(self, element):
+        """
+        Function to convert an heading tag into a proper
+        element in our moin_page namespace
+        """
+        heading_level = element.tag[1]
+        key = moin_page('outline-level')
+        attrib = {}
+        attrib[key] = heading_level
+        return self.new_copy(moin_page.h, element, attrib)
+
+    def visit_br(self, element):
+        return moin_page.line_break()
+
+    def visit_big(self, element):
+        key = moin_page('font-size')
+        attrib = {}
+        attrib[key] = '120%'
+        return self.new_copy(moin_page.span, element, attrib)
+
+    def visit_small(self, element):
+        key = moin_page('font-size')
+        attrib = {}
+        attrib[key] = '85%'
+        return self.new_copy(moin_page.span, element, attrib)
+
+    def visit_sub(self, element):
+        key = moin_page('baseline-shift')
+        attrib = {}
+        attrib[key] = 'sub'
+        return self.new_copy(moin_page.span, element, attrib)
+
+    def visit_sup(self, element):
+        key = moin_page('baseline-shift')
+        attrib = {}
+        attrib[key] = 'super'
+        return self.new_copy(moin_page.span, element, attrib)
+
+    def visit_u(self, element):
+        key = moin_page('text-decoration')
+        attrib = {}
+        attrib[key] = 'underline'
+        return self.new_copy(moin_page.span, element, attrib)
+
+    def visit_ins(self, element):
+        key = moin_page('text-decoration')
+        attrib = {}
+        attrib[key] = 'underline'
+        return self.new_copy(moin_page.span, element, attrib)
+
+    def visit_del(self, element):
+        key = moin_page('text-decoration')
+        attrib = {}
+        attrib[key] = 'line-through'
+        return self.new_copy(moin_page.span, element, attrib)
+
+    def visit_s(self, element):
+        key = moin_page('text-decoration')
+        attrib = {}
+        attrib[key] = 'line-through'
+        return self.new_copy(moin_page.span, element, attrib)
+
+    def visit_strike(self, element):
+        key = moin_page('text-decoration')
+        attrib = {}
+        attrib[key] = 'line-through'
+        return self.new_copy(moin_page.span, element, attrib)
+
+    def visit_hr(self, element, min_class=u'moin-hr1', max_class=u'moin-hr6', default_class=u'moin-hr3'):
+        hr_class = element.attrib.get('class')
+        if not (min_class <= hr_class <= max_class):
+            element.attrib[html('class')] = default_class
+        return self.new_copy(moin_page.separator, element, {})
+
+    def visit_img(self, element):
+        """
+        <img src="URI" /> --> <object xlink:href="URI />
+        """
+        key = xlink('href')
+        attrib = {}
+        attrib[key] = element.attrib.get("src")
+        return moin_page.object(attrib)
+
+    def visit_object(self, element):
+        """
+        <object data="href"></object> --> <object xlink="href" />
+        """
+        key = xlink('href')
+        attrib = {}
+        if self.base_url:
+            attrib[key] = ''.join([self.base_url, element.get(html.data)])
+        else:
+            attrib[key] = element.get(html.data)
+
+        # Convert the href attribute into unicode
+        attrib[key] = unicode(attrib[key])
+        return moin_page.object(attrib)
+
+    def visit_inline(self, element):
+        """
+        For some specific inline tags (defined in inline_tags)
+        We just return <span element="tag.name">
+        """
+        key = html('class')
+        attrib = {}
+        attrib[key] = ''.join(['html-', element.tag.name])
+        return self.new_copy(moin_page.span, element, attrib)
+
+    def visit_li(self, element):
+        """
+        NB : A list item (<li>) is like the following snippet :
+        <list-item>
+            <list-item-label>label</list-item-label>
+            <list-item-body>Body</list-item-body>
+        </list-item>
+
+        For <li> element, there is no label
+        """
+        list_item_body = ET.Element(moin_page.list_item_body,
+                                    attrib={}, children=self.do_children(element))
+        return ET.Element(moin_page.list_item, attrib={}, children=[list_item_body])
+
+    def visit_list(self, element):
+        """
+        Convert a list of item (whatever the type : ordered or unordered)
+        So we have a html code like :
+        <ul>
+            <li>Item 1</li>
+            <li>Item 2</li>
+        </ul>
+
+        Which will be convert like :
+        <list>
+            <list-item>
+                <list-item-body>Item 1</list-item-body>
+            </list-item>
+            <list-item>
+                <list-item-body>Item 2</list-item-body>
+            </list-item>
+        </list>
+        """
+        # We will define the appropriate attribute
+        # according to the type of the list
+        attrib = {}
+        if element.tag == "ul" or element.tag == "dir":
+            attrib[moin_page('item-label-generate')] = 'unordered'
+        elif element.tag == "ol":
+            attrib[moin_page('item-label-generate')] = 'ordered'
+
+        return ET.Element(moin_page.list, attrib=attrib,
+                children=self.do_children(element))
+
+    def visit_a(self, element):
+        key = xlink('href')
+        attrib = {}
+        href = postproc_text(self.markdown, element.attrib.get("href"))
+        if allowed_uri_scheme(href):
+            attrib[key] = href
+        else:
+            return href
+        return self.new_copy(moin_page.a, element, attrib)
+
+    def visit(self, element):
+        # Our element can be converted directly, just by changing the namespace
+        if element.tag in self.symmetric_tags:
+            return self.new_copy_symmetric(element, attrib={})
+
+        # Our element is enough simple to just change the tag name
+        if element.tag in self.simple_tags:
+            return self.new_copy(self.simple_tags[element.tag], element, attrib={})
+
+        # Our element defines a list
+        if element.tag in self.list_tags:
+            return self.visit_list(element)
+
+        # We convert our element as a span tag with element attribute
+        if element.tag in self.inline_tags:
+            return self.visit_inline(element)
+
+        # We have a heading tag
+        if self.heading_re.match(element.tag):
+            return self.visit_heading(element)
+
+        # Otherwise we need a specific procedure to handle it
+        method_name = 'visit_' + element.tag
+        method = getattr(self, method_name, None)
+        if method:
+            return method(element)
+
+        # We should ignore this tag
+        if element.tag in self.ignored_tags:
+            logging.info("INFO : Ignored tag : {0}".format(element.tag))
+            return
+
+        logging.info("INFO : Unhandled tag : {0}".format(element.tag))
+        return
+
+    def do_children(self, element):
+        new = []
+        if hasattr(element, "text") and element.text is not None:
+            new.append(postproc_text(self.markdown, element.text))
+
+        for child in element:
+            r = self.visit(child)
+            if r is None:
+                r = ()
+            elif not isinstance(r, (list, tuple)):
+                r = (r, )
+            new.extend(r)
+            if hasattr(child, "tail") and child.tail is not None:
+                new.append(postproc_text(self.markdown, child.tail))
+        return new
+
+    # }}}
+
+    def __init__(self):
+        self.markdown = Markdown()
+
+    @classmethod
+    def _factory(cls, input, output, **kw):
+        return cls()
+
+    def __call__(self, data, contenttype=None, arguments=None):
+        text = decode_data(data, contenttype)
+
+        # {{{ stolen from Markdown.convert
+
+        # Fixup the source text
+        try:
+            text = unicode(text)
+        except UnicodeDecodeError, e:
+            # Customise error message while maintaining original traceback
+            e.reason += '. -- Note: Markdown only accepts unicode input!'
+            raise
+
+        text = text.replace(md_util.STX, "").replace(md_util.ETX, "")
+        text = text.replace("\r\n", "\n").replace("\r", "\n") + "\n\n"
+        text = re.sub(r'\n\s+\n', '\n\n', text)
+        text = text.expandtabs(8)
+
+        # Split into lines and run the line preprocessors.
+        lines = text.split("\n")
+        for prep in self.markdown.preprocessors.values():
+            lines = prep.run(lines)
+
+        # Parse the high-level elements.
+        md_root = self.markdown.parser.parseDocument(lines).getroot()
+
+        # Run the tree-processors
+        for treeprocessor in self.markdown.treeprocessors.values():
+            new_md_root = treeprocessor.run(md_root)
+            if new_md_root:
+                md_root = new_md_root
+
+        # }}}
+
+        # md_root is a list of plain old Python ElementTree objects.
+
+        converted = self.do_children(md_root)
+        body = moin_page.body(children=converted)
+        root = moin_page.page(children=[body])
+
+        return root
+
+from . import default_registry
+from MoinMoin.util.mime import Type, type_moin_document
+default_registry.register(Converter._factory, Type("text/x-markdown"), type_moin_document)
+default_registry.register(Converter._factory, Type('x-moin/format;name=markdown'), type_moin_document)
+
+# vim: foldmethod=marker
--- a/MoinMoin/converter/mediawiki_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/mediawiki_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -22,12 +22,11 @@
 from MoinMoin import config
 from MoinMoin.util.iri import Iri
 from MoinMoin.util.tree import html, moin_page, xlink
-from MoinMoin.converter.moinwiki_in import _Iter, _Stack
 
 from ._args import Arguments
 from ._args_wiki import parse as parse_arguments
 from ._wiki_macro import ConverterMacro
-from ._util import decode_data, normalize_split_text
+from ._util import decode_data, normalize_split_text, _Iter, _Stack
 
 
 class _TableArguments(object):
@@ -142,7 +141,6 @@
         stack.clear()
         stack.top_append(moin_page.separator())
 
-
     block_table = r"""
         ^
         (?P<table>
@@ -184,7 +182,7 @@
 
         element = moin_page.table_body()
         stack.push(element)
-        lines = _Iter(self.block_table_lines(iter_content))
+        lines = _Iter(self.block_table_lines(iter_content), startno=iter_content.lineno)
         element = moin_page.table_row()
         stack.push(element)
         preprocessor_status = []
@@ -333,7 +331,7 @@
             if list_definition:
                 element_label = moin_page.list_item_label()
                 stack.top_append(element_label)
-                new_stack = _Stack(element_label)
+                new_stack = _Stack(element_label, iter_content=iter_content)
                 # TODO: definition list doesn't work,
                 #       if definition of the term on the next line
                 splited_text = text.split(':')
@@ -346,13 +344,13 @@
             element_body.level, element_body.type = level, type
 
             stack.push(element_body)
-            new_stack = _Stack(element_body)
+            new_stack = _Stack(element_body, iter_content=iter_content)
         else:
             new_stack = stack
             level = 0
 
         is_list = list_begin
-        iter = _Iter(self.indent_iter(iter_content, text, level, is_list))
+        iter = _Iter(self.indent_iter(iter_content, text, level, is_list), startno=iter_content.lineno)
         for line in iter:
             match = self.block_re.match(line)
             it = iter
@@ -815,7 +813,7 @@
     class Mediawiki_preprocessor(object):
 
         class Preprocessor_tag(object):
-            def __init__(self, name='', text='', tag='',  status=True):
+            def __init__(self, name='', text='', tag='', status=True):
                 self.tag_name = name
                 self.tag = tag
                 self.text = [text]
@@ -852,7 +850,7 @@
             self.nowiki_tag = ''
             self._stack = []
 
-        def push(self, status = []):
+        def push(self, status=[]):
             self._stack.append(self.opened_tags)
             self.opened_tags = status
             if self.opened_tags:
@@ -877,7 +875,7 @@
                     self.nowiki_tag = ''
             return self.opened_tags
 
-        def __call__(self, line, tags = []):
+        def __call__(self, line, tags=[]):
             tags = tags or self.opened_tags
             match = re.match(r"(.*?)(\<.*\>.*)|(.*)", line)
             if match:
@@ -897,11 +895,14 @@
                         tag = match.group(1)
                         next_text = match.group(3)
                         text = match.group(2) or match.group(4)
-                        if not text: text = ''
+                        if not text:
+                            text = ''
                         tag_match = re.match(r"/\s*(.*)", tag)
                         status = not tag_match
-                        if tag_match: tag_name = tag_match.group(1).split(' ')[0]
-                        else: tag_name = tag.split(' ')[0]
+                        if tag_match:
+                            tag_name = tag_match.group(1).split(' ')[0]
+                        else:
+                            tag_name = tag.split(' ')[0]
                         if not tag_name in self.all_tags or re.match(r'.*/\s*$', tag)\
                                 or self.nowiki and (status or tag_name != self.nowiki_tag):
                             if not len(tags):
@@ -948,7 +949,6 @@
                 return ''.join(post_line)
             self.opened_tags = tags
 
-
     def _apply(self, match, prefix, *args):
         """
         Call the _repl method for the last matched group with the given prefix.
@@ -969,7 +969,7 @@
 
         body = moin_page.body(attrib=attrib)
 
-        stack = _Stack(body)
+        stack = _Stack(body, iter_content=iter_content)
 
         for line in iter_content:
             match = self.indent_re.match(line)
@@ -1004,4 +1004,3 @@
 from MoinMoin.util.mime import Type, type_moin_document
 default_registry.register(Converter.factory, Type('x-moin/format;name=mediawiki'), type_moin_document)
 default_registry.register(Converter.factory, Type('text/x-mediawiki'), type_moin_document)
-
--- a/MoinMoin/converter/moinwiki19_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/moinwiki19_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -162,5 +162,3 @@
 from MoinMoin.util.mime import Type, type_moin_document, type_moin_wiki
 default_registry.register(ConverterFormat19.factory, Type('text/x.moin.wiki;format=1.9'), type_moin_document)
 default_registry.register(ConverterFormat19.factory, Type('x-moin/format;name=wiki;format=1.9'), type_moin_document)
-
-
--- a/MoinMoin/converter/moinwiki_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/moinwiki_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -27,92 +27,7 @@
 from ._args import Arguments
 from ._args_wiki import parse as parse_arguments
 from ._wiki_macro import ConverterMacro
-from ._util import decode_data, normalize_split_text
-
-
-class _Iter(object):
-    """
-    Iterator with push back support
-
-    Collected items can be pushed back into the iterator and further calls will
-    return them.
-    """
-
-    def __init__(self, parent):
-        self.__finished = False
-        self.__parent = iter(parent)
-        self.__prepend = []
-
-    def __iter__(self):
-        return self
-
-    def next(self):
-        if self.__finished:
-            raise StopIteration
-
-        if self.__prepend:
-            return self.__prepend.pop(0)
-
-        try:
-            return self.__parent.next()
-        except StopIteration:
-            self.__finished = True
-            raise
-
-    def push(self, item):
-        self.__prepend.append(item)
-
-
-class _Stack(object):
-    class Item(object):
-        def __init__(self, elem):
-            self.elem = elem
-            if elem.tag.uri == moin_page:
-                self.name = elem.tag.name
-            else:
-                self.name = None
-
-    def __init__(self, bottom=None):
-        self._list = []
-        if bottom:
-            self._list.append(self.Item(bottom))
-
-    def __len__(self):
-        return len(self._list)
-
-    def clear(self):
-        del self._list[1:]
-
-    def pop(self):
-        self._list.pop()
-
-    def pop_name(self, *names):
-        """
-        Remove anything from the stack including the given node.
-        """
-        while len(self._list) > 2 and not self.top_check(*names):
-            self.pop()
-        self.pop()
-
-    def push(self, elem):
-        self.top_append(elem)
-        self._list.append(self.Item(elem))
-
-    def top(self):
-        return self._list[-1].elem
-
-    def top_append(self, elem):
-        self.top().append(elem)
-
-    def top_append_ifnotempty(self, elem):
-        if elem:
-            self.top_append(elem)
-
-    def top_check(self, *names):
-        """
-        Checks if the name of the top of the stack matches the parameters.
-        """
-        return self._list[-1].name in names
+from ._util import decode_data, normalize_split_text, _Iter, _Stack
 
 
 class _TableArguments(object):
@@ -178,7 +93,6 @@
     def number_rows_spanned_repl(self, args, number_rows_spanned):
         args.keyword['number-rows-spanned'] = int(number_rows_spanned)
 
-
     def add_attr_to_style(self, args, attr):
         args.keyword['style'] = args.keyword.get('style', "") + attr + " "
 
@@ -352,7 +266,7 @@
 
         nowiki_marker_len = len(nowiki_marker)
 
-        lines = _Iter(self.block_nowiki_lines(iter_content, nowiki_marker_len))
+        lines = _Iter(self.block_nowiki_lines(iter_content, nowiki_marker_len), startno=iter_content.lineno)
 
         if nowiki_interpret:
             if nowiki_args:
@@ -361,7 +275,7 @@
                 args = Arguments(keyword={'_old': nowiki_args_old})
             else:
                 args = None
-
+            logging.debug("nowiki_name: %r" % nowiki_name)
             # Parse it directly if the type is ourself
             if not nowiki_name or nowiki_name == 'wiki':
                 body = self.parse_block(lines, args)
@@ -382,12 +296,12 @@
 
     block_separator = r'(?P<separator> ^ \s* -{4,} \s* $ )'
 
-    def block_separator_repl(self, _iter_content, stack, separator, hr_class = u'moin-hr{0}'):
+    def block_separator_repl(self, _iter_content, stack, separator, hr_class=u'moin-hr{0}'):
         stack.clear()
         hr_height = min((len(separator) - 3), 6)
         hr_height = max(hr_height, 1)
         attrib = {moin_page('class'): hr_class.format(hr_height)}
-        elem = moin_page.separator(attrib = attrib)
+        elem = moin_page.separator(attrib=attrib)
         stack.top_append(elem)
 
     block_table = r"""
@@ -552,7 +466,7 @@
             if list_definition_text:
                 element_label = moin_page.list_item_label()
                 stack.top_append(element_label)
-                new_stack = _Stack(element_label)
+                new_stack = _Stack(element_label, iter_content=iter_content)
 
                 self.parse_inline(list_definition_text, new_stack, self.inline_re)
 
@@ -560,11 +474,11 @@
             element_body.level, element_body.type = level, type
 
             stack.push(element_body)
-            new_stack = _Stack(element_body)
+            new_stack = _Stack(element_body, iter_content=iter_content)
         else:
             new_stack = stack
 
-        iter = _Iter(self.indent_iter(iter_content, text, level))
+        iter = _Iter(self.indent_iter(iter_content, text, level), startno=iter_content.lineno)
         for line in iter:
             match = self.block_re.match(line)
             it = iter
@@ -1111,7 +1025,7 @@
 
         body = moin_page.body(attrib=attrib)
 
-        stack = _Stack(body)
+        stack = _Stack(body, iter_content=iter_content)
 
         for line in iter_content:
             data = dict(((str(k), v) for k, v in self.indent_re.match(line).groupdict().iteritems() if v is not None))
@@ -1138,5 +1052,3 @@
 from MoinMoin.util.mime import Type, type_moin_document, type_moin_wiki
 default_registry.register(Converter.factory, type_moin_wiki, type_moin_document)
 default_registry.register(Converter.factory, Type('x-moin/format;name=wiki'), type_moin_document)
-
-
--- a/MoinMoin/converter/moinwiki_out.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/moinwiki_out.py	Sat Oct 06 17:46:24 2012 +0200
@@ -422,10 +422,10 @@
         if text_decoration == u'underline':
             return Moinwiki.underline + self.open_children(elem) + Moinwiki.underline
         if font_size:
-            return u"{0}{1}{2}".format(Moinwiki.larger_open if font_size == u"120%"\
+            return u"{0}{1}{2}".format(Moinwiki.larger_open if font_size == u"120%"
                                            else Moinwiki.smaller_open,
                                        self.open_children(elem),
-                                       Moinwiki.larger_close if font_size == u"120%"\
+                                       Moinwiki.larger_close if font_size == u"120%"
                                            else Moinwiki.smaller_close)
         if baseline_shift == u'super':
             return u'^{0}^'.format(u''.join(elem.itertext()))
@@ -518,4 +518,3 @@
 from MoinMoin.util.mime import Type, type_moin_document, type_moin_wiki
 default_registry.register(Converter.factory, type_moin_document, type_moin_wiki)
 default_registry.register(Converter.factory, type_moin_document, Type('x-moin/format;name=wiki'))
-
--- a/MoinMoin/converter/nonexistent_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/nonexistent_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -28,10 +28,10 @@
         attrib = {
             xlink.href: Iri(scheme='wiki', authority='', path='/'+item_name, query='do=modify'),
         }
-        return moin_page.a(attrib=attrib, children=[_("%(item_name)s does not exist. Create it?", item_name=item_name)])
-
+        a = moin_page.a(attrib=attrib, children=[_("%(item_name)s does not exist. Create it?", item_name=item_name)])
+        body = moin_page.body(children=(a, ))
+        return moin_page.page(children=(body, ))
 
 from . import default_registry
 from MoinMoin.util.mime import Type, type_moin_document
 default_registry.register(Converter._factory, Type('application/x-nonexistent'), type_moin_document)
-
--- a/MoinMoin/converter/opendocument_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/opendocument_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -70,4 +70,3 @@
 
 for t in openoffice_types:
     default_registry.register(OpenOfficeIndexingConverter._factory, Type(t), type_text_plain)
-
--- a/MoinMoin/converter/pygments_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/pygments_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -52,7 +52,6 @@
             if lastval:
                 self._append(lasttype, lastval, element)
 
-
     class Converter(object):
         @classmethod
         def _factory(cls, type_input, type_output, **kw):
@@ -84,6 +83,7 @@
                 else:
                     pygments_name = None
 
+            logging.debug("pygments_name: %r" % pygments_name)
             if pygments_name:
                 lexer = pygments.lexers.find_lexer_class(pygments_name)
                 return cls(lexer())
@@ -113,15 +113,10 @@
             body = moin_page.body(children=(blockcode, ))
             return moin_page.page(children=(body, ))
 
-
     from . import default_registry
     from MoinMoin.util.mime import Type, type_moin_document
-    # Pygments type detection is rather expensive, therefore we want to register
-    # after all normal parsers but before the compatibility parsers and wildcard
-    default_registry.register(Converter._factory, Type(type='text'), type_moin_document,
-                              default_registry.PRIORITY_MIDDLE + 1)
-    default_registry.register(Converter._factory, Type('x-moin/format'), type_moin_document,
-                              default_registry.PRIORITY_MIDDLE + 1)
+    default_registry.register(Converter._factory, Type(type='text'), type_moin_document)
+    default_registry.register(Converter._factory, Type('x-moin/format'), type_moin_document)
 
 else:
     # we have no Pygments, minimal Converter replacement, so highlight view does not crash
@@ -138,4 +133,3 @@
                 blockcode.append(line.expandtabs())
             body = moin_page.body(children=(blockcode, ))
             return moin_page.page(children=(body, ))
-
--- a/MoinMoin/converter/rst_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/rst_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -443,7 +443,7 @@
             self.open_moin_page_node(
                 moin_page.part(
                     attrib={
-                        moin_page.content_type:\
+                        moin_page.content_type:
                             "x-moin/macro;name={0}".format(macro_name)}))
             if arguments:
                 self.open_moin_page_node(moin_page.arguments())
@@ -783,4 +783,3 @@
                           Type('text/x-rst'), type_moin_document)
 default_registry.register(Converter.factory,
                           Type('x-moin/format;name=rst'), type_moin_document)
-
--- a/MoinMoin/converter/rst_out.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/rst_out.py	Sat Oct 06 17:46:24 2012 +0200
@@ -336,9 +336,9 @@
                     child =\
                         re.sub(r"\n(.)", lambda m: u"\n{0}{1}".format(u' '*(len(u''.join(self.list_item_labels)) + len(self.list_item_labels)), m.group(1)), child)
                     if self.last_closed == "p":
-                        childrens_output.append(u'\n'\
-                                + u' '\
-                                * (len(''.join(self.list_item_labels))\
+                        childrens_output.append(u'\n'
+                                + u' '
+                                * (len(''.join(self.list_item_labels))
                                    + len(self.list_item_labels)))
                 elif self.status[-1] == "text":
                     if self.last_closed == "p":
@@ -393,8 +393,8 @@
     def open_moinpage_blockcode(self, elem):
         text = u''.join(elem.itertext())
         max_subpage_lvl = 3
-        text = text.replace(u'\n', u'\n  '\
-                                  + u' ' * (len(u''.join(self.list_item_labels))\
+        text = text.replace(u'\n', u'\n  '
+                                  + u' ' * (len(u''.join(self.list_item_labels))
                                          + len(self.list_item_labels)))
 
         if self.list_level >= 0:
@@ -434,10 +434,10 @@
 
     def open_moinpage_line_break(self, elem):
         if self.status[-1] == "list":
-            return ReST.linebreak\
-                   + u' '\
-                     * (len(u''.join(self.list_item_labels))\
-                        + len(self.list_item_labels))
+            return (ReST.linebreak
+                    + u' '
+                      * (len(u''.join(self.list_item_labels))
+                         + len(self.list_item_labels)))
         if self.last_closed == 'p':
             return u'\n\n'
         return ReST.linebreak
@@ -472,9 +472,9 @@
         if self.list_item_labels[-1] == u'' or self.list_item_labels[-1] == u' ':
             self.list_item_labels[-1] = u' '
             self.list_item_label = self.list_item_labels[-1] + u' '
-            ret = u' '\
-                  * (len(u''.join(self.list_item_labels[:-1]))\
-                     + len(self.list_item_labels[:-1]))
+            ret = (u' '
+                   * (len(u''.join(self.list_item_labels[:-1]))
+                      + len(self.list_item_labels[:-1])))
             if self.last_closed and self.last_closed != 'list':
                 ret = u'\n{0}'.format(ret)
             return ret + self.open_children(elem)
@@ -484,15 +484,15 @@
         ret = u''
         if self.last_closed:
             ret = u'\n'
-        ret += u' ' * (len(u''.join(self.list_item_labels[:-1]))\
-                      + len(self.list_item_labels[:-1]))\
-               + self.list_item_label
+        ret += (u' ' * (len(u''.join(self.list_item_labels[:-1]))
+                       + len(self.list_item_labels[:-1]))
+                + self.list_item_label)
         if self.list_item_labels[-1] in [u'1.', u'i.', u'I.', u'a.', u'A.']:
             self.list_item_labels[-1] = u'#.'
 
         ret = self.define_references() + ret + self.open_children(elem)
         if self.last_closed == "text":
-            return  ret + u'\n'
+            return ret + u'\n'
         return ret
 
     def open_moinpage_note(self, elem):
@@ -505,7 +505,7 @@
         return u' [#]_ '
 
     def open_moinpage_object(self, elem):
-        # TODO: object parametrs support
+        # TODO: object parameters support
         href = elem.get(xlink.href, u'')
         href = href.split(u'?')
         args = u''
@@ -557,17 +557,17 @@
                                 and self.last_closed != 'list_item_header'\
                                 and self.last_closed != 'list_item_footer'\
                                 and self.last_closed != 'p':
-                ret = ReST.linebreak + u' '\
-                                        * (len(u''.join(self.list_item_labels))\
-                                           + len(self.list_item_labels)) + self.open_children(elem)
+                ret = (ReST.linebreak + u' '
+                                        * (len(u''.join(self.list_item_labels))
+                                           + len(self.list_item_labels)) + self.open_children(elem))
             elif self.last_closed and self.last_closed == 'p':
                 #return ReST.p +\
-                ret = u"\n" + u' ' * (len(u''.join(self.list_item_labels))\
-                                   + len(self.list_item_labels)) + self.open_children(elem)
+                ret = (u"\n" + u' ' * (len(u''.join(self.list_item_labels))
+                                    + len(self.list_item_labels)) + self.open_children(elem))
             else:
                 ret = self.open_children(elem)
             if not self.delete_newlines:
-                ret +=  u"\n"
+                ret += u"\n"
         else:
             self.status.append('p')
             ret = self.open_children(elem)
@@ -750,11 +750,11 @@
         """
         ret = u''
         self.all_used_references.extend(self.used_references)
-        definitions = [u" " * (len(u''.join(self.list_item_labels))\
-                                    + len(self.list_item_labels))\
+        definitions = [u" " * (len(u''.join(self.list_item_labels))
+                                    + len(self.list_item_labels))
                                   + u".. _{0}: {1}".format(t, h) for t, h in self.used_references]
-        definitions.extend(u" " * (len(u''.join(self.list_item_labels))\
-                                     + len(self.list_item_labels))\
+        definitions.extend(u" " * (len(u''.join(self.list_item_labels))
+                                     + len(self.list_item_labels))
                                   + link for link in self.objects)
         definition_block = u"\n\n".join(definitions)
 
@@ -778,4 +778,3 @@
 default_registry.register(Converter.factory,
                           type_moin_document,
                           Type('x-moin/format;name=rst'))
-
--- a/MoinMoin/converter/smiley.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/smiley.py	Sat Oct 06 17:46:24 2012 +0200
@@ -139,4 +139,3 @@
 from . import default_registry
 from MoinMoin.util.mime import type_moin_document
 default_registry.register(Converter._factory, type_moin_document, type_moin_document)
-
--- a/MoinMoin/converter/text_csv_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/text_csv_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -12,6 +12,7 @@
 
 from ._table import TableMixin
 from ._util import decode_data, normalize_split_text
+from MoinMoin.util.tree import moin_page
 
 
 class Converter(TableMixin):
@@ -39,10 +40,11 @@
                 row.append(encoded_cell.decode('utf-8'))
             if row:
                 rows.append(row)
-        return self.build_dom_table(rows)
+        table = self.build_dom_table(rows)
+        body = moin_page.body(children=(table, ))
+        return moin_page.page(children=(body, ))
 
 
 from . import default_registry
 from MoinMoin.util.mime import Type, type_moin_document
 default_registry.register(Converter._factory, Type('text/csv'), type_moin_document)
-
--- a/MoinMoin/converter/text_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/text_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -42,6 +42,6 @@
 
 from . import default_registry
 from MoinMoin.util.mime import Type, type_moin_document
+# Assign a lower priority (= bigger number) so that it is tried after pygments_in
 default_registry.register(Converter._factory, Type(type='text'), type_moin_document,
-                          default_registry.PRIORITY_MIDDLE + 2)
-
+                          default_registry.PRIORITY_MIDDLE + 1)
--- a/MoinMoin/converter/text_out.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/text_out.py	Sat Oct 06 17:46:24 2012 +0200
@@ -30,4 +30,3 @@
 from . import default_registry
 from MoinMoin.util.mime import Type, type_moin_document, type_text_plain
 default_registry.register(Converter.factory, type_moin_document, type_text_plain)
-
--- a/MoinMoin/converter/xml_in.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/converter/xml_in.py	Sat Oct 06 17:46:24 2012 +0200
@@ -40,4 +40,3 @@
 from . import default_registry
 from MoinMoin.util.mime import Type, type_text_plain
 default_registry.register(XMLIndexingConverter._factory, Type('text/xml'), type_text_plain)
-
--- a/MoinMoin/datastruct/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/datastruct/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -16,4 +16,3 @@
 
 from MoinMoin.datastruct.backends import GroupDoesNotExistError
 from MoinMoin.datastruct.backends import DictDoesNotExistError
-
--- a/MoinMoin/datastruct/backends/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/datastruct/backends/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -80,7 +80,7 @@
         raise NotImplementedError()
 
     def __repr__(self):
-        return "<{0} groups={1}>".format(self.__class__, list(self))
+        return "<{0!r} groups={1!r}>".format(self.__class__, list(self))
 
     def _retrieve_members(self, group_name):
         raise NotImplementedError()
@@ -250,7 +250,7 @@
                     yield group_name
 
     def __repr__(self):
-        return "<{0} name={1} members={2} member_groups={3}>".format(self.__class__, self.name, self.members, self.member_groups)
+        return "<{0!r} name={1!r} members={2!r} member_groups={3!r}>".format(self.__class__, self.name, self.members, self.member_groups)
 
 
 class BaseDict(object, DictMixin):
@@ -330,4 +330,3 @@
             return self[key]
         except DictDoesNotExistError:
             return default
-
--- a/MoinMoin/datastruct/backends/_tests/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/datastruct/backends/_tests/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -29,7 +29,6 @@
                    u'EmptyGroup': [],
                    u'CheckNotExistingGroup': [u'NotExistingGroup']}
 
-
     expanded_groups = {u'EditorGroup': [u'Admin1', u'Admin2', u'John',
                                         u'JoeDoe', u'Editor1'],
                        u'AdminGroup': [u'Admin1', u'Admin2', u'John'],
@@ -178,11 +177,9 @@
         assert dicts.get(u'SomeNotExistingDict') is None
         assert dicts.get(u'SomeNotExistingDict', {}) == {}
 
-
         for dict_name, expected_dict in self.dicts.items():
             test_dict = dicts[dict_name]
             for key, value in expected_dict.items():
                 assert u'SomeNotExistingKey' not in test_dict
                 assert test_dict.get(u'SomeNotExistingKey') is None
                 assert test_dict.get(u'SomeNotExistingKey', {}) == {}
-
--- a/MoinMoin/datastruct/backends/_tests/test_config_dicts.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/datastruct/backends/_tests/test_config_dicts.py	Sat Oct 06 17:46:24 2012 +0200
@@ -6,7 +6,7 @@
 """
 
 
-from  MoinMoin.datastruct.backends._tests import DictsBackendTest
+from MoinMoin.datastruct.backends._tests import DictsBackendTest
 from MoinMoin.datastruct import ConfigDicts
 from MoinMoin._tests import wikiconfig
 
@@ -27,4 +27,3 @@
             assert result in expected
 
 coverage_modules = ['MoinMoin.datastruct.backends.config_dicts']
-
--- a/MoinMoin/datastruct/backends/_tests/test_config_groups.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/datastruct/backends/_tests/test_config_groups.py	Sat Oct 06 17:46:24 2012 +0200
@@ -21,4 +21,3 @@
 
 
 coverage_modules = ['MoinMoin.datastruct.backends.config_groups']
-
--- a/MoinMoin/datastruct/backends/_tests/test_wiki_dicts.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/datastruct/backends/_tests/test_wiki_dicts.py	Sat Oct 06 17:46:24 2012 +0200
@@ -41,4 +41,3 @@
         assert result == expected
 
 coverage_modules = ['MoinMoin.datastruct.backends.wiki_dicts']
-
--- a/MoinMoin/datastruct/backends/_tests/test_wiki_groups.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/datastruct/backends/_tests/test_wiki_groups.py	Sat Oct 06 17:46:24 2012 +0200
@@ -107,4 +107,3 @@
         assert has_rights_after, 'AnotherUser must have read rights because after appenditem he is member of NewGroup'
 
 coverage_modules = ['MoinMoin.datastruct.backends.wiki_groups']
-
--- a/MoinMoin/datastruct/backends/composite_dicts.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/datastruct/backends/composite_dicts.py	Sat Oct 06 17:46:24 2012 +0200
@@ -44,4 +44,3 @@
 
     def __repr__(self):
         return "<{0} backends={1}>".format(self.__class__, self._backends)
-
--- a/MoinMoin/datastruct/backends/composite_groups.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/datastruct/backends/composite_groups.py	Sat Oct 06 17:46:24 2012 +0200
@@ -67,4 +67,3 @@
 
     def __repr__(self):
         return "<{0} backends={1}>".format(self.__class__, self._backends)
-
--- a/MoinMoin/datastruct/backends/config_dicts.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/datastruct/backends/config_dicts.py	Sat Oct 06 17:46:24 2012 +0200
@@ -36,4 +36,3 @@
             return self._dicts[dict_name]
         except KeyError:
             raise DictDoesNotExistError(dict_name)
-
--- a/MoinMoin/datastruct/backends/config_groups.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/datastruct/backends/config_groups.py	Sat Oct 06 17:46:24 2012 +0200
@@ -41,4 +41,3 @@
             return self._groups[group_name]
         except KeyError:
             raise GroupDoesNotExistError(group_name)
-
--- a/MoinMoin/datastruct/backends/wiki_dicts.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/datastruct/backends/wiki_dicts.py	Sat Oct 06 17:46:24 2012 +0200
@@ -45,4 +45,3 @@
         rev = item.get_revision(CURRENT)
         somedict = rev.meta.get(SOMEDICT, {})
         return somedict
-
--- a/MoinMoin/datastruct/backends/wiki_groups.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/datastruct/backends/wiki_groups.py	Sat Oct 06 17:46:24 2012 +0200
@@ -53,4 +53,3 @@
         rev = item[CURRENT]
         usergroup = rev.meta.get(USERGROUP, [])
         return usergroup
-
--- a/MoinMoin/error.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/error.py	Sat Oct 06 17:46:24 2012 +0200
@@ -105,4 +105,3 @@
 
 class InternalError(FatalError):
     """ Raise when internal fatal error is found """
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/forms.py	Sat Oct 06 17:46:24 2012 +0200
@@ -0,0 +1,205 @@
+# Copyright: 2012 MoinMoin:PavelSviderski
+# Copyright: 2012 MoinMoin:CheerXiao
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+    MoinMoin - Flatland widgets
+
+    General Flatland widgets containing hints for the templates.
+"""
+
+
+import re, datetime
+import json
+
+from flatland import Element, Form, String, Integer, Boolean, Enum, Dict, JoinedString, List, Array, DateTime as _DateTime
+from flatland.util import class_cloner, Unspecified
+from flatland.validation import Validator, Present, IsEmail, ValueBetween, URLValidator, Converted, ValueAtLeast
+from flatland.exc import AdaptationError
+
+from flask import g as flaskg
+
+from MoinMoin.constants.forms import *
+from MoinMoin.constants.keys import ITEMID, NAME
+from MoinMoin.i18n import _, L_, N_
+from MoinMoin.security.textcha import TextCha, TextChaizedForm, TextChaValid
+from MoinMoin.util.forms import FileStorage
+
+
+Text = String.with_properties(widget=WIDGET_TEXT)
+
+MultilineText = String.with_properties(widget=WIDGET_MULTILINE_TEXT)
+
+OptionalText = Text.using(optional=True)
+
+RequiredText = Text.validated_by(Present())
+
+OptionalMultilineText = MultilineText.using(optional=True)
+
+RequiredMultilineText = MultilineText.validated_by(Present())
+
+
+class ValidJSON(Validator):
+    """Validator for JSON
+    """
+    invalid_json_msg = L_('Invalid JSON.')
+
+    def validate(self, element, state):
+        try:
+            json.loads(element.value)
+        except: # catch ANY exception that happens due to unserializing
+            return self.note_error(element, state, 'invalid_json_msg')
+        return True
+
+JSON = OptionalMultilineText.with_properties(lang='en', dir='ltr').validated_by(ValidJSON())
+
+URL = String.with_properties(widget=WIDGET_TEXT).validated_by(URLValidator())
+
+OpenID = URL.using(label=L_('OpenID')).with_properties(placeholder=L_("OpenID address"))
+
+YourOpenID = OpenID.with_properties(placeholder=L_("Your OpenID address"))
+
+Email = String.using(label=L_('E-Mail')).with_properties(widget=WIDGET_EMAIL, placeholder=L_("E-Mail address")).validated_by(IsEmail())
+
+YourEmail = Email.with_properties(placeholder=L_("Your E-Mail address"))
+
+Password = Text.with_properties(widget=WIDGET_PASSWORD).using(label=L_('Password'))
+
+RequiredPassword = Password.validated_by(Present())
+
+Checkbox = Boolean.with_properties(widget=WIDGET_CHECKBOX).using(optional=True, default=1)
+
+InlineCheckbox = Checkbox.with_properties(widget=WIDGET_INLINE_CHECKBOX)
+
+Select = Enum.with_properties(widget=WIDGET_SELECT)
+
+
+# Need a better name to capture the behavior
+class MyJoinedString(JoinedString):
+    """
+    A JoinedString that offers the list of children (not the joined string) as
+    value property.
+    """
+    @property
+    def value(self):
+        return [child.value for child in self]
+
+    @property
+    def u(self):
+        return self.separator.join(child.u for child in self)
+
+Tags = MyJoinedString.of(String).with_properties(widget=WIDGET_TEXT).using(label=L_('Tags'), optional=True, separator=', ', separator_regex=re.compile(r'\s*,\s*'))
+
+Search = Text.using(default=u'', optional=True).with_properties(widget=WIDGET_SEARCH, placeholder=L_("Search Query"))
+
+_Integer = Integer.validated_by(Converted())
+
+AnyInteger = _Integer.with_properties(widget=WIDGET_ANY_INTEGER)
+
+Natural = AnyInteger.validated_by(ValueAtLeast(0))
+
+SmallNatural = _Integer.with_properties(widget=WIDGET_SMALL_NATURAL)
+
+
+class DateTimeUNIX(_DateTime):
+    """
+    A DateTime that uses a UNIX timestamp instead of datetime as internal
+    representation of DateTime.
+    """
+    def serialize(self, value):
+        """Serializes value to string."""
+        if isinstance(value, int):
+            try:
+                value = datetime.datetime.utcfromtimestamp(value)
+            except ValueError:
+                pass
+        return super(DateTimeUNIX, self).serialize(value)
+
+    def adapt(self, value):
+        """Coerces value to a native UNIX timestamp.
+
+        If value is an instance of int and it is a correct UNIX timestamp,
+        returns it unchanged. Otherwise uses DateTime superclass to parse it.
+        """
+        if isinstance(value, int):
+            try:
+                # check if a value is a correct timestamp
+                dt = datetime.datetime.utcfromtimestamp(value)
+                return value
+            except ValueError:
+                raise AdaptationError()
+        dt = super(DateTimeUNIX, self).adapt(value)
+        if isinstance(dt, datetime.datetime):
+            # XXX forces circular dependency when it is in the head import block
+            from MoinMoin.themes import utctimestamp
+            # TODO: Add support for timezones
+            dt = utctimestamp(dt)
+        return dt
+
+DateTime = (DateTimeUNIX.with_properties(widget=WIDGET_DATETIME, placeholder=_("YYYY-MM-DD HH:MM:SS (example: 2013-12-31 23:59:59)"))
+            .validated_by(Converted(incorrect=L_("Please use the following format: YYYY-MM-DD HH:MM:SS"))))
+
+File = FileStorage.with_properties(widget=WIDGET_FILE)
+
+Submit = String.using(default=L_('OK'), optional=True).with_properties(widget=WIDGET_SUBMIT, class_=CLASS_BUTTON)
+
+Hidden = String.using(optional=True).with_properties(widget=WIDGET_HIDDEN)
+
+# optional=True is needed to get rid of the "required field" indicator on the UI (usually an asterisk)
+ReadonlyStringList = List.of(String).using(optional=True).with_properties(widget=WIDGET_READONLY_STRING_LIST)
+
+ReadonlyItemLinkList = ReadonlyStringList.with_properties(widget=WIDGET_READONLY_ITEM_LINK_LIST)
+
+
+# XXX When some user chooses a Reference candidate that is removed before the
+# user POSTs, the validator fails. This can be confusing.
+class ValidReference(Validator):
+    """
+    Validator for Reference
+    """
+    invalid_reference_msg = L_('Invalid Reference.')
+
+    def validate(self, element, state):
+        if element.value not in element.valid_values:
+            return self.note_error(element, state, 'invalid_reference_msg')
+        return True
+
+class Reference(Select.with_properties(empty_label=L_(u'(None)')).validated_by(ValidReference())):
+    """
+    A metadata property that points to another item selected out of the
+    Results of a search query.
+    """
+    @class_cloner
+    def to(cls, query, query_args={}):
+        cls._query = query
+        cls._query_args = query_args
+        return cls
+
+    @classmethod
+    def _get_choices(cls):
+        revs = flaskg.storage.search(cls._query, **cls._query_args)
+        choices = [(rev.meta[ITEMID], rev.meta[NAME]) for rev in revs]
+        if cls.optional:
+            choices.append((u'', cls.properties['empty_label']))
+        return choices
+
+    def __init__(self, value=Unspecified, **kw):
+        super(Reference, self).__init__(value, **kw)
+        # NOTE There is a slight chance of two instances of the same Reference
+        # subclass having different set of choices when the storage changes
+        # between their initialization.
+        choices = self._get_choices()
+        self.properties['labels'] = dict(choices)
+        self.valid_values = [id_ for id_, name in choices]
+
+
+class BackReference(ReadonlyItemLinkList):
+    """
+    Back references built from Whoosh query.
+    """
+    def set(self, query, **query_args):
+        revs = flaskg.storage.search(query, **query_args)
+        super(BackReference, self).set([rev.meta[NAME] for rev in revs])
+
+
+MultiSelect = Array.with_properties(widget=WIDGET_MULTI_SELECT)
--- a/MoinMoin/i18n/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/i18n/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -18,7 +18,7 @@
 
 from flask import current_app, request
 from flask import g as flaskg
-from flaskext.babel import Babel, gettext, ngettext, lazy_gettext
+from flask.ext.babel import Babel, gettext, ngettext, lazy_gettext
 
 _ = gettext
 N_ = ngettext
@@ -66,4 +66,3 @@
     u = getattr(flaskg, 'user', None)
     if u and u.timezone is not None:
         return u.timezone
-
--- a/MoinMoin/i18n/_tests/test_i18n.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/i18n/_tests/test_i18n.py	Sat Oct 06 17:46:24 2012 +0200
@@ -31,4 +31,3 @@
     assert result1 == 'text1'
     result2 = N_('text1', 'text2', 2)
     assert result2 == 'text2'
-
--- a/MoinMoin/items/__init__.py	Tue Mar 27 23:48:23 2012 +0200
+++ b/MoinMoin/items/__init__.py	Sat Oct 06 17:46:24 2012 +0200
@@ -1,3 +1,4 @@
+# Copyright: 2012 MoinMoin:CheerXiao
 # Copyright: 2009 MoinMoin:ThomasWaldmann
 # Copyright: 2009-2011 MoinMoin:ReimarBauer
 # Copyright: 2009 MoinMoin:ChristopherDenter
@@ -7,150 +8,107 @@
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """
-    MoinMoin - misc. mimetype items
+    MoinMoin - high-level (frontend) items
 
     While MoinMoin.storage cares for backend storage of items,
     this module cares for more high-level, frontend items,
     e.g. showing, editing, etc. of wiki items.
-"""
-# TODO: split this huge module into multiple ones after code has stabilized
 
-import os, re, time, datetime, base64
-import tarfile
-import zipfile
-import tempfile
+    Each class in this module corresponds to an itemtype.
+"""
+
+import time
 import itertools
 import types
+import json
 from StringIO import StringIO
-from array import array
+from collections import namedtuple
+from operator import attrgetter
 
-from flatland import Form, String, Integer, Boolean, Enum
-from flatland.validation import Validator, Present, IsEmail, ValueBetween, URLValidator, Converted
+from flask import current_app as app
+from flask import g as flaskg
+from flask import request, Response, redirect, abort, escape
+
+from flatland import Form
+
+from jinja2 import Markup
 
 from whoosh.query import Term, And, Prefix
 
-from MoinMoin.util.forms import FileStorage
-
-from MoinMoin.security.textcha import TextCha, TextChaizedForm, TextChaValid
-from MoinMoin.signalling import item_modified
-from MoinMoin.util.mimetype import MimeType
-from MoinMoin.util.mime import Type, type_moin_document
-from MoinMoin.util.tree import moin_page, html, xlink, docbook
-from MoinMoin.util.iri import Iri
-from MoinMoin.util.crypto import cache_key
-from MoinMoin.storage.middleware.protecting import AccessDenied
-
-try:
-    import PIL
-    from PIL import Image as PILImage
-    from PIL.ImageChops import difference as PILdiff
-except ImportError:
-    PIL = None
-
 from MoinMoin import log
 logging = log.getLogger(__name__)
 
-try:
-    import json
-except ImportError:
-    import simplejson as json
-
-from flask import current_app as app
-from flask import g as flaskg
-
-from flask import request, url_for, flash, Response, redirect, abort, escape
+from MoinMoin.security.textcha import TextCha, TextChaizedForm
+from MoinMoin.signalling import item_modified
+from MoinMoin.storage.middleware.protecting import AccessDenied
+from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError, StorageError
+from MoinMoin.i18n import L_
+from MoinMoin.themes import render_template
+from MoinMoin.util.mime import Type
+from MoinMoin.util.interwiki import url_for_item
+from MoinMoin.util.registry import RegistryBase
+from MoinMoin.util.clock import timed
+from MoinMoin.forms import RequiredText, OptionalText, JSON, Tags, Submit
+from MoinMoin.constants.keys import (
+    NAME, NAME_OLD, NAME_EXACT, WIKINAME, MTIME, SYSITEM_VERSION, ITEMTYPE,
+    CONTENTTYPE, SIZE, ACTION, ADDRESS, HOSTNAME, USERID, COMMENT,
+    HASH_ALGORITHM, ITEMID, REVID, DATAID, CURRENT, PARENTID
+    )
+from MoinMoin.constants.contenttypes import charset
 
-from werkzeug import is_resource_modified
-from jinja2 import Markup
+from .content import content_registry, Content, NonExistentContent, Draw
 
-from MoinMoin.i18n import _, L_, N_
-from MoinMoin.themes import render_template
-from MoinMoin import wikiutil, config, user
-from MoinMoin.util.send_file import send_file
-from MoinMoin.util.interwiki import url_for_item
-from MoinMoin.storage.error import NoSuchItemError, NoSuchRevisionError, StorageError
-from MoinMoin.config import NAME, NAME_OLD, NAME_EXACT, WIKINAME, MTIME, REVERTED_TO, ACL, \
-                            IS_SYSITEM, SYSITEM_VERSION,  USERGROUP, SOMEDICT, \
-                            CONTENTTYPE, SIZE, LANGUAGE, ITEMLINKS, ITEMTRANSCLUSIONS, \
-                            TAGS, ACTION, ADDRESS, HOSTNAME, USERID, EXTRA, COMMENT, \
-                            HASH_ALGORITHM, CONTENTTYPE_GROUPS, ITEMID, REVID, DATAID, \
-                            CURRENT, PARENTID
 
 COLS = 80
-ROWS_DATA = 20
 ROWS_META = 10
 
 
-from ..util.registry import RegistryBase
-
-
 class RegistryItem(RegistryBase):
-    class Entry(object):
-        def __init__(self, factory, content_type, priority):
-            self.factory = factory
-            self.content_type = content_type
-            self.priority = priority
-
-        def __call__(self, name, content_type, kw):
-            if self.content_type.issupertype(content_type):
-                return self.factory(name, content_type, **kw)
-
-        def __eq__(self, other):
-            if isinstance(other, self.__class__):
-                return (self.factory == other.factory and
-                        self.content_type == other.content_type and
-                        self.priority == other.priority)
-            return NotImplemented
+    class Entry(namedtuple('Entry', 'factory itemtype display_name description order')):
+        def __call__(self, itemtype, *args, **kw):
+            if self.itemtype == itemtype:
+                return self.factory(*args, **kw)
 
         def __lt__(self, other):
             if isinstance(other, self.__class__):
-                if self.priority < other.priority:
-                    return True
-                if self.content_type != other.content_type:
-                    return other.content_type.issupertype(self.content_type)
-                return False
+                return self.itemtype < other.itemtype
             return NotImplemented
 
-        def __repr__(self):
-            return '<{0}: {1}, prio {2} [{3!r}]>' % (self.__class__.__name__,
-                    self.content_type,
-                    self.priority,
-                    self.factory)
+    def __init__(self):
+        super(RegistryItem, self).__init__()
+        self.shown_entries = []
 
-    def get(self, name, content_type, **kw):
-        for entry in self._entries:
-            item = entry(name, content_type, kw)
-            if item is not None:
-                return item
-
-    def register(self, factory, content_type, priority=RegistryBase.PRIORITY_MIDDLE):
+    def register(self, e, shown):
         """
         Register a factory
 
         :param factory: Factory to register. Callable, must return an object.
         """
-        return self._register(self.Entry(factory, content_type, priority))
+        if shown:
+            self.shown_entries.append(e)
+            self.shown_entries.sort(key=attrgetter('order'))
+        return self._register(e)
 
 
 item_registry = RegistryItem()
 
-
-def conv_serialize(doc, namespaces):
-    out = array('u')
-    flaskg.clock.start('conv_serialize')
-    doc.write(out.fromunicode, namespaces=namespaces, method='xml')
-    out = out.tounicode()
-    flaskg.clock.stop('conv_serialize')
-    return out
+def register(cls):
+    item_registry.register(RegistryItem.Entry(cls._factory, cls.itemtype, cls.display_name, cls.description, cls.order), cls.shown)
+    return cls
 
 
 class DummyRev(dict):
     """ if we have no stored Revision, we use this dummy """
-    def __init__(self, item, contenttype):
+    def __init__(self, item, itemtype=None, contenttype=None):
         self.item = item
-        self.meta = {CONTENTTYPE: contenttype}
+        self.meta = {
+            ITEMTYPE: itemtype or u'nonexistent',
+            CONTENTTYPE: contenttype or u'application/x-nonexistent'
+        }
         self.data = StringIO('')
         self.revid = None
+        if self.item:
+            self.meta[NAME] = [self.item.name]
 
 
 class DummyItem(object):
@@ -163,165 +121,153 @@
         return True
 
 
-class Item(object):
-    """ Highlevel (not storage) Item """
+def get_storage_revision(name, itemtype=None, contenttype=None, rev_id=CURRENT, item=None):
+    """
+    Get a storage Revision.
+
+    If :item is supplied it is used as the storage Item; otherwise the storage
+    Item is looked up with :name. If it is not found (either because the item
+    doesn't exist or the user does not have the required permissions) a
+    DummyItem is created, and a DummyRev is created with appropriate metadata
+    properties and the "item" property pointing to the DummyItem. The DummyRev
+    is then returned.
+
+    If the previous step didn't end up with a DummyRev, the revision
+    designated by :rev_id is then looked up. If it is not found, current
+    revision is looked up and returned instead. If current revision is not
+    found (i.e. the item has no revision), a DummyRev is created. (TODO: in
+    the last two cases, emit warnings or throw exceptions.)
+
+    :itemtype and :contenttype are used when creating a DummyRev, where
+    metadata is not available from the storage.
+    """
+    if 1: # try:
+        if item is None:
+            item = flaskg.storage[name]
+        else:
+            name = item.name
+    if not item: # except NoSuchItemError:
+        logging.debug("No such item: {0!r}".format(name))
+        item = DummyItem(name)
+        rev = DummyRev(item, itemtype, contenttype)
+        logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(name, contenttype))
+    else:
+        logging.debug("Got item: {0!r}".format(name))
+        try:
+            rev = item.get_revision(rev_id)
+        except KeyError: # NoSuchRevisionError:
+            try:
+                rev = item.get_revision(CURRENT) # fall back to current revision
+                # XXX add some message about invalid revision
+            except KeyError: # NoSuchRevisionError:
+                logging.debug("Item {0!r} has no revisions.".format(name))
+                rev = DummyRev(item, itemtype, contenttype)
+                logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(name, contenttype))
+        logging.debug("Got item {0!r}, revision: {1!r}".format(name, rev_id))
+    return rev
+
+
+class BaseChangeForm(TextChaizedForm):
+    comment = OptionalText.using(label=L_('Comment')).with_properties(placeholder=L_("Comment about your change"))
+    submit = Submit
+
+
+class BaseMetaForm(Form):
+    itemtype = RequiredText.using(label=L_("Item type")).with_properties(placeholder=L_("Item type"))
+    contenttype = RequiredText.using(label=L_("Content type")).with_properties(placeholder=L_("Content type"))
+    # Disabled - Flatland doesn't distinguish emtpy value and nonexistent
+    # value, while an emtpy acl and no acl have different semantics
+    #acl = OptionalText.using(label=L_('ACL')).with_properties(placeholder=L_("Access Control List"))
+    summary = OptionalText.using(label=L_("Summary")).with_properties(placeholder=L_("One-line summary of the item"))
+    tags = Tags
+
+
+class BaseModifyForm(BaseChangeForm):
     @classmethod
-    def _factory(cls, name=u'', contenttype=None, **kw):
-        return cls(name, contenttype=unicode(contenttype), **kw)
+    def from_item(cls, item):
+        form = cls.from_defaults()
+        TextCha(form).amend_form()
+        form._load(item)
+        return form
 
     @classmethod
-    def create(cls, name=u'', contenttype=None, rev_id=CURRENT, item=None):
-        if contenttype is None:
-            contenttype = u'application/x-nonexistent'
+    def from_request(cls, request):
+        form = cls.from_flat(request.form.items() + request.files.items())
+        TextCha(form).amend_form()
+        return form
 
-        if 1: # try:
-            if item is None:
-                item = flaskg.storage[name]
-            else:
-                name = item.name
-        if not item: # except NoSuchItemError:
-            logging.debug("No such item: {0!r}".format(name))
-            item = DummyItem(name)
-            rev = DummyRev(item, contenttype)
-            logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(name, contenttype))
-        else:
-            logging.debug("Got item: {0!r}".format(name))
-            try:
-                rev = item.get_revision(rev_id)
-                contenttype = u'application/octet-stream' # it exists
-            except KeyError: # NoSuchRevisionError:
-                try:
-                    rev = item.get_revision(CURRENT) # fall back to current revision
-                    # XXX add some message about invalid revision
-                except KeyError: # NoSuchRevisionError:
-                    logging.debug("Item {0!r} has no revisions.".format(name))
-                    rev = DummyRev(item, contenttype)
-                    logging.debug("Item {0!r}, created dummy revision with contenttype {1!r}".format(name, contenttype))
-            logging.debug("Got item {0!r}, revision: {1!r}".format(name, rev_id))
+
+IndexEntry = namedtuple('IndexEntry', 'relname meta')
+
+MixedIndexEntry = namedtuple('MixedIndexEntry', 'relname meta hassubitems')
+
+class Item(object):
+    """ Highlevel (not storage) Item, wraps around a storage Revision"""
+    # placeholder values for registry entry properties
+    itemtype = ''
+    display_name = u''
+    description = u''
+    shown = True
+    order = 0
+
+    @classmethod
+    def _factory(cls, *args, **kw):
+        return cls(*args, **kw)
+
+    @classmethod
+    def create(cls, name=u'', itemtype=None, contenttype=None, rev_id=CURRENT, item=None):
+        """
+        Create a highlevel Item by looking up :name or directly wrapping
+        :item and extract the Revision designated by :rev_id revision.
+
+        The highlevel Item is created by creating an instance of Content
+        subclass according to the item's contenttype metadata entry; The
+        :contenttype argument can be used to override contenttype. It is used
+        only when handling +convert (when deciding the contenttype of target
+        item), +modify (when creating a new item whose contenttype is not yet
+        decided), +diff and +diffraw (to coerce the Content to a common
+        super-contenttype of both revisions).
+
+        After that the Content instance, an instance of Item subclass is
+        created according to the item's itemtype metadata entry, and the
+        previously created Content instance is assigned to its content
+        property.
+        """
+        rev = get_storage_revision(name, itemtype, contenttype, rev_id, item)
         contenttype = rev.meta.get(CONTENTTYPE) or contenttype # use contenttype in case our metadata does not provide CONTENTTYPE
         logging.debug("Item {0!r}, got contenttype {1!r} from revision meta".format(name, contenttype))
         #logging.debug("Item %r, rev meta dict: %r" % (name, dict(rev.meta)))
-        item = item_registry.get(name, Type(contenttype), rev=rev)
-        logging.debug("ItemClass {0!r} handles {1!r}".format(item.__class__, contenttype))
+
+        # XXX Cannot pass item=item to Content.__init__ via
+        # content_registry.get yet, have to patch it later.
+        content = Content.create(contenttype)
+
+        itemtype = rev.meta.get(ITEMTYPE) or itemtype or u'default'
+        logging.debug("Item {0!r}, got itemtype {1!r} from revision meta".format(name, itemtype))
+
+        item = item_registry.get(itemtype, name, rev=rev, content=content)
+        logging.debug("Item class {0!r} handles {1!r}".format(item.__class__, itemtype))
+
+        content.item = item
+
         return item
 
-    def __init__(self, name, rev=None, contenttype=None):
+    def __init__(self, name, rev=None, content=None):
         self.name = name
         self.rev = rev
-        self.contenttype = contenttype
+        self.content = content
 
     def get_meta(self):
         return self.rev.meta
     meta = property(fget=get_meta)
 
-    def _render_meta(self):
-        # override this in child classes
-        return ''
-
-    def internal_representation(self, converters=['smiley']):
-        """
-        Return the internal representation of a document using a DOM Tree
-        """
-        flaskg.clock.start('conv_in_dom')
-        hash_name = HASH_ALGORITHM
-        hash_hexdigest = self.rev.meta.get(hash_name)
-        if hash_hexdigest:
-            cid = cache_key(usage="internal_representation",
-                            hash_name=hash_name,
-                            hash_hexdigest=hash_hexdigest)
-            doc = app.cache.get(cid)
-        else:
-            # likely a non-existing item
-            doc = cid = None
-        if doc is None:
-            # We will see if we can perform the conversion:
-            # FROM_mimetype --> DOM
-            # if so we perform the transformation, otherwise we don't
-            from MoinMoin.converter import default_registry as reg
-            input_conv = reg.get(Type(self.contenttype), type_moin_document)
-            if not input_conv:
-                raise TypeError("We cannot handle the conversion from {0} to the DOM tree".format(self.contenttype))
-            smiley_conv = reg.get(type_moin_document, type_moin_document,
-                    icon='smiley')
-
-            # We can process the conversion
-            links = Iri(scheme='wiki', authority='', path='/' + self.name)
-            doc = input_conv(self.rev, self.contenttype)
-            # XXX is the following assuming that the top element of the doc tree
-            # is a moin_page.page element? if yes, this is the wrong place to do that
-            # as not every doc will have that element (e.g. for images, we just get
-            # moin_page.object, for a tar item, we get a moin_page.table):
-            doc.set(moin_page.page_href, unicode(links))
-            for conv in converters:
-                if conv == 'smiley':
-                    doc = smiley_conv(doc)
-            if cid:
-                app.cache.set(cid, doc)
-        flaskg.clock.stop('conv_in_dom')
-        return doc
+    # XXX Backward compatibility, remove soon
+    @property
+    def contenttype(self):
+        return self.content.contenttype if self.content else None
 
-    def _expand_document(self, doc):
-        from MoinMoin.converter import default_registry as reg
-        include_conv = reg.get(type_moin_document, type_moin_document, includes='expandall')
-        macro_conv = reg.get(type_moin_document, type_moin_document, macros='expandall')
-        link_conv = reg.get(type_moin_document, type_moin_document, links='extern')
-        flaskg.clock.start('conv_include')
-        doc = include_conv(doc)
-        flaskg.clock.stop('conv_include')
-        flaskg.clock.start('conv_macro')
-        doc = macro_conv(doc)
-        flaskg.clock.stop('conv_macro')
-        flaskg.clock.start('conv_link')
-        doc = link_conv(doc)
-        flaskg.clock.stop('conv_link')
-        return doc
-
-    def _render_data(self):
-        from MoinMoin.converter import default_registry as reg
-        include_conv = reg.get(type_moin_document, type_moin_document, includes='expandall')
-        macro_conv = reg.get(type_moin_document, type_moin_document, macros='expandall')
-        # TODO: Real output format
-        html_conv = reg.get(type_moin_document, Type('application/x-xhtml-moin-page'))
-        doc = self.internal_representation()
-        doc = self._expand_document(doc)
-        flaskg.clock.start('conv_dom_html')
-        doc = html_conv(doc)
-        flaskg.clock.stop('conv_dom_html')
-        rendered_data = conv_serialize(doc, {html.namespace: ''})
-        # This is a work-around to avoid the invalid <div /> tag from being passed
-        # and causing layout issues in many browsers
-        # Instead, send a <div></div> tag which is valid according to the HTML spec
-        # The wider issue with serialization is covered here:
-        # https://bitbucket.org/thomaswaldmann/moin-2.0/issue/145/xml-mode-serialization-returns-self
-        return "<div></div>" if rendered_data == "<div xmlns=\"http://www.w3.org/1999/xhtml\" />" \
-                             else rendered_data
-
-    def _render_data_xml(self):
-        doc = self.internal_representation()
-        return conv_serialize(doc,
-                              {moin_page.namespace: '',
-                               xlink.namespace: 'xlink',
-                               html.namespace: 'html',
-                               })
-
-    def _render_data_highlight(self):
-        # override this in child classes
-        return ''
-
-    def _do_modify_show_templates(self):
-        # call this if the item is still empty
-        rev_ids = []
-        item_templates = self.get_templates(self.contenttype)
-        return render_template('modify_show_template_selection.html',
-                               item_name=self.name,
-                               rev=self.rev,
-                               contenttype=self.contenttype,
-                               templates=item_templates,
-                               first_rev_id=rev_ids and rev_ids[0],
-                               last_rev_id=rev_ids and rev_ids[-1],
-                               meta_rendered='',
-                               data_rendered='',
-                               )
+    def _render_meta(self):
+        return "<pre>{0}</pre>".format(escape(self.meta_dict_to_text(self.meta, use_filter=False)))
 
     def meta_filter(self, meta):
         """ kill metadata entries that we set automatically when saving """
@@ -364,33 +310,15 @@
             meta[PARENTID] = revid
         return meta
 
-    def get_data(self):
-        return '' # TODO create a better method for binary stuff
-    data = property(fget=get_data)
-
-    def _write_stream(self, content, new_rev, bufsize=8192):
-        written = 0
-        if hasattr(content, "read"):
-            while True:
-                buf = content.read(bufsize)
-                if not buf:
-                    break
-                new_rev.data.write(buf)
-                written += len(buf)
-        elif isinstance(content, str):
-            new_rev.data.write(content)
-            written += len(content)
-        else:
-            raise StorageError("unsupported content object: {0!r}".format(content))
-        return written
-
     def _rename(self, name, comment, action, delete=False):
-        new_name = name
-        self._save(self.meta, self.data, name=new_name, action=action, comment=comment, delete=delete)
-        for child in self.get_index():
-            item = Item.create(child[0])
-            new_name = u'/'.join((name, child[1]))
-            item._save(item.meta, item.data, name=new_name, action=action, comment=comment, delete=delete)
+        self._save(self.meta, self.content.data, name=name, action=action, comment=comment, delete=delete)
+        old_prefixlen = len(self.subitems_prefix)
+        new_prefix = name + '/'  # XXX BROKEN for name==None
+        for child in self.get_subitem_revs():
+            child_oldname = child.meta[NAME]  # XXX BROKEN - this is a list of names now
+            child_newname = new_prefix + child_oldname[old_prefixlen:]
+            item = Item.create(child_oldname)
+            item._save(item.meta, item.content.data, name=child_newname, action=action, comment=comment, delete=delete)
 
     def rename(self, name, comment=u''):
         """
@@ -404,10 +332,8 @@
         """
         return self._rename(None, comment, action=u'TRASH', delete=True)
 
-    def revert(self):
-        # called from revert UI/POST
-        comment = request.form.get('comment')
-        self._save(self.meta, self.data, action=u'REVERT', comment=comment)
+    def revert(self, comment=u''):
+        return self._save(self.meta, self.content.data, action=u'REVERT', comment=comment)
 
     def destroy(self, comment=u'', destroy_item=False):
         # called from destroy UI/POST
@@ -418,45 +344,47 @@
             # just destroy this revision
             self.rev.item.destroy_revision(self.rev.revid)
 
-    def modify(self):
-        # called from modify UI/POST
-        meta = data = contenttype_guessed = None
-        contenttype_qs = request.values.get('contenttype')
-        data_file = request.files.get('data_file')
-        if data_file and data_file.filename: # XXX is this the right way to check if there was a file uploaded?
-            data = data_file.stream
-            # this is likely a guess by the browser, based on the filename
-            contenttype_guessed = data_file.content_type # comes from form multipart data
-        if data is None:
-            # no file upload, try taking stuff from textarea
-            data = request.form.get('data_text')
-            if data is not None:
-                # there was a data_text field with (possibly empty) content
-                assert isinstance(data, unicode) # we get unicode from the form
-                data = self.data_form_to_internal(data)
-                data = self.data_internal_to_storage(data)
-                # we know it is text and utf-8 - XXX is there a way to get the charset of the form?
-                contenttype_guessed = u'text/plain;charset=utf-8'
-        # data might be None here, if we have a form with just the data_file field, no file was uploaded
-        # and no data_text field. this can happen if just metadata of a non-text item is edited.
-
-        meta_text = request.form.get('meta_text')
-        if meta_text is not None:
-            # there was a meta_text field with (possibly empty) content
-            # Note: if you get crashes here, please see the ValidJSON validator
-            # to catch invalid json issues early.
-            meta = self.meta_text_to_dict(meta_text)
-        if meta is None:
-            # no form metadata - reuse some stuff from previous metadata?
-            meta = {}
-
+    def modify(self, meta, data, comment=u'', contenttype_guessed=None, contenttype_qs=None):
         if contenttype_qs:
             # we use querystring param to FORCE content type
             meta[CONTENTTYPE] = contenttype_qs
 
-        comment = request.form.get('comment')
         return self._save(meta, data, contenttype_guessed=contenttype_guessed, comment=comment)
 
+    class _ModifyForm(BaseModifyForm):
+        """Base class for ModifyForm of Item subclasses."""
+        meta_form = BaseMetaForm
+        extra_meta_text = JSON.using(label=L_("Extra MetaData (JSON)")).with_properties(rows=ROWS_META, cols=COLS)
+        meta_template = 'modify_meta.html'
+
+        def _load(self, item):
+            meta = item.prepare_meta_for_modify(item.meta)
+            # Default value of `policy` argument of Flatland.Dict.set's is
+            # 'strict', which causes KeyError to be thrown when meta contains
+            # meta keys that are not present in self['meta_form']. Setting
+            # policy to 'duck' suppresses this behavior.
+            self['meta_form'].set(meta, policy='duck')
+            for k in self['meta_form'].field_schema_mapping.keys():
+                meta.pop(k, None)
+            self['extra_meta_text'].set(item.meta_dict_to_text(meta))
+            self['content_form']._load(item.content)
+
+        def _dump(self, item):
+            meta = self['meta_form'].value.copy()
+            meta.update(item.meta_text_to_dict(self['extra_meta_text'].value))
+            data, contenttype_guessed = self['content_form']._dump(item.content)
+            comment = self['comment'].value
+            return meta, data, contenttype_guessed, comment
+
+    def do_modify(self):
+        """
+        Handle +modify requests, both GET and POST.
+
+        This method should be overridden in subclasses, providing polymorphic
+        behavior for the +modify view.
+        """
+        raise NotImplementedError
+
     def _save(self, meta, data=None, name=None, action=u'SAVE', contenttype_guessed=None, comment=u'',
               overwrite=False, delete=False):
         backend = flaskg.storage
@@ -508,7 +436,7 @@
                 data = ''
 
         if isinstance(data, unicode):
-            data = data.encode(config.charset) # XXX wrong! if contenttype gives a coding, we MUST use THAT.
+            data = data.encode(charset) # XXX wrong! if contenttype gives a coding, we MUST use THAT.
 
         if isinstance(data, str):
             data = StringIO(data)
@@ -521,128 +449,125 @@
         item_modified.send(app._get_current_object(), item_name=name)
         return newrev.revid, newrev.meta[SIZE]
 
-    def get_index(self):
-        """ create an index of sub items of this item """
-        if self.name:
-            prefix = self.name + u'/'
-            query = And([Term(WIKINAME, app.cfg.interwikiname), Prefix(NAME_EXACT, prefix)])
-        else:
-            # trick: an item of empty name can be considered as "virtual root item",
-            # that has all wiki items as sub items
-            prefix = u''
-            query = Term(WIKINAME, app.cfg.interwikiname)
-        # We only want the sub-item part of the item names, not the whole item objects.
-        prefix_len = len(prefix)
-        revs = flaskg.storage.search(query, limit=None)
-        items = []
-        for rev in revs:
-            rev.set_context(self.name)
-            items.append((rev.name, rev.name[prefix_len:], rev.meta[CONTENTTYPE]))
-
-        return sorted(items, key=lambda item: item[0])
-
-    def _connect_levels(self, index):
-        new_index = []
-        last = self.name
-        for item in index:
-            name = item[0]
-
-            while not name.startswith(last):
-                last = last.rpartition('/')[0]
-
-            missing_layers = name.split('/')[last.count('/')+1:-1]
-
-            for layer in missing_layers:
-                last = '/'.join([last, layer])
-                new_index.append((last, last[len(self.name)+1:], u'application/x-nonexistent'))
-
-            last = item[0]
-            new_index.append(item)
-
-        return new_index
-
-    def flat_index(self, startswith=None, selected_groups=None):
-        """
-        creates a top level index of sub items of this item
-        if startswith is set, filtering is done on the basis of starting letter of item name
-        if selected_groups is set, items whose contentype belonging to the selected contenttype_groups, are filtered.
-        """
-        index = self.get_index()
-        index = self._connect_levels(index)
+    @property
+    def subitems_prefix(self):
+        return self.name + u'/' if self.name else u''
 
-        all_ctypes = [[ctype for ctype, clabel in contenttypes]
-                      for gname, contenttypes in CONTENTTYPE_GROUPS]
-        all_ctypes_chain = itertools.chain(*all_ctypes)
-        all_contenttypes = list(all_ctypes_chain)
-        contenttypes_without_encoding = [contenttype[:contenttype.index(u';')]
-                                         for contenttype in all_contenttypes
-                                         if u';' in contenttype]
-        all_contenttypes.extend(contenttypes_without_encoding) # adding more mime-types without the encoding term
-
-        if selected_groups:
-            ctypes = [[ctype for ctype, clabel in contenttypes]
-                      for gname, contenttypes in CONTENTTYPE_GROUPS
-                      if gname in selected_groups]
-            ctypes_chain = itertools.chain(*ctypes)
-            selected_contenttypes = list(ctypes_chain)
-            contenttypes_without_encoding = [contenttype[:contenttype.index(u';')]
-                                             for contenttype in selected_contenttypes
-                                             if u';' in contenttype]
-            selected_contenttypes.extend(contenttypes_without_encoding)
-        else:
-            selected_contenttypes = all_contenttypes
+    @timed()
+    def get_subitem_revs(self):
+        """
+        Create a list of subitems of this item.
 
-        unknown_item_group = "unknown items"
-        if startswith:
-            startswith = (u'{0}'.format(startswith), u'{0}'.format(startswith.swapcase()))
-            if not selected_groups or unknown_item_group in selected_groups:
-                index = [(fullname, relname, contenttype)
-                         for fullname, relname, contenttype in index
-                         if u'/' not in relname
-                         and relname.startswith(startswith)
-                         and (contenttype not in all_contenttypes or contenttype in selected_contenttypes)]
-                         # If an item's contenttype not present in the default contenttype list,
-                         # then it will be shown without going through any filter.
+        Subitems are in the form of storage Revisions.
+        """
+        query = Term(WIKINAME, app.cfg.interwikiname)
+        # trick: an item of empty name can be considered as "virtual root item"
+        # that has all wiki items as sub items
+        if self.name:
+            query = And([query, Prefix(NAME_EXACT, self.subitems_prefix)])
+        revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None)
+        return revs
+
+    @timed()
+    def make_flat_index(self, subitems):
+        """
+        Create two IndexEntry lists - ``dirs`` and ``files`` - from a list of
+        subitems.
+
+        Direct subitems are added to the ``files`` list.
+
+        For indirect subitems, its ancestor which is a direct subitem is added
+        to the ``dirs`` list. Supposing current index root is 'foo' and when
+        'foo/bar/la' is encountered, 'foo/bar' is added to ``dirs``.
+
+        The direct subitem need not exist.
+
+        When both a subitem itself and some of its subitems are in the subitems
+        list, it appears in both ``files`` and ``dirs``.
+        """
+        prefix = self.subitems_prefix
+        prefixlen = len(prefix)
+        # IndexEntry instances of "file" subitems
+        files = []
+        # IndexEntry instances of "directory" subitems
+        dirs = []
+        added_dir_relnames = set()
+
+        for rev in subitems:
+            fullname = rev.meta[NAME]  # XXX BROKEN, this is a list of names now
+            relname = fullname[prefixlen:]
+            if '/' in relname:
+                # Find the *direct* subitem that is the ancestor of current
+                # (indirect) subitem. e.g. suppose when the index root is
+                # 'foo', and current item (`rev`) is 'foo/bar/lorem/ipsum',
+                # 'foo/bar' will be found.
+                direct_relname = relname.partition('/')[0]
+                if direct_relname not in added_dir_relnames:
+                    added_dir_relnames.add(direct_relname)
+                    direct_fullname = prefix + direct_relname
+                    direct_rev = get_storage_revision(direct_fullname)
+                    dirs.append(IndexEntry(direct_relname, direct_rev.meta))
             else:
-                index = [(fullname, relname, contenttype)
-                         for fullname, relname, contenttype in index
-                         if u'/' not in relname
-                         and relname.startswith(startswith)
-                         and (contenttype in selected_contenttypes)]
+                files.append(IndexEntry(relname, rev.meta))
 
-        else:
-            if not selected_groups or unknown_item_group in selected_groups:
-                index = [(fullname, relname, contenttype)
-                         for fullname, relname, contenttype in index
-                         if u'/' not in relname
-                         and (contenttype not in all_contenttypes or contenttype in selected_contenttypes)]
-            else:
-                index = [(fullname, relname, contenttype)
-                         for fullname, relname, contenttype in index
-                         if u'/' not in relname
-                         and contenttype in selected_contenttypes]
+        return dirs, files
 
+    @timed()
+    def filter_index(self, index, startswith=None, selected_groups=None):
+        """
+        Filter a list of IndexEntry.
+
+        :param startswith: if set, only items whose names start with startswith
+                           are selected.
+        :param selected_groups: if set, only items whose contentypes belong to
+                                the selected contenttype_groups are selected.
+        """
+        if startswith is not None:
+            index = [e for e in index
+                     if e.relname.startswith((startswith, startswith.swapcase()))]
+
+        def build_contenttypes(groups):
+            contenttypes = []
+            for g in groups:
+                entries = content_registry.groups.get(g, []) # .get is a temporary workaround for "unknown items" group
+                contenttypes.extend([e.content_type for e in entries])
+            return contenttypes
+
+        def contenttype_match(tested, cts):
+            for ct in cts:
+                if ct.issupertype(tested):
+                    return True
+            return False
+
+        if selected_groups is not None:
+            selected_contenttypes = build_contenttypes(selected_groups)
+            filtered_index = [e for e in index if contenttype_match(Type(e.meta[CONTENTTYPE]), selected_contenttypes)]
+
+            unknown_item_group = "unknown items"
+            if unknown_item_group in selected_groups:
+                all_contenttypes = build_contenttypes(content_registry.group_names)
+                filtered_index.extend([e for e in index
+                                       if not contenttype_match(Type(e.meta[CONTENTTYPE]), all_contenttypes)])
+
+            index = filtered_index
         return index
 
+    def get_index(self, startswith=None, selected_groups=None):
+        dirs, files = self.make_flat_index(self.get_subitem_revs())
+        return dirs, self.filter_index(files, startswith, selected_groups)
+
+    def get_mixed_index(self):
+        dirs, files = self.make_flat_index(self.get_subitem_revs())
+        dirs_dict = dict([(e.relname, MixedIndexEntry(*e, hassubitems=True)) for e in dirs])
+        index_dict = dict([(e.relname, MixedIndexEntry(*e, hassubitems=False)) for e in files])
+        index_dict.update(dirs_dict)
+        return sorted(index_dict.values())
+
     index_template = 'index.html'
 
-    def get_detailed_index(self, index):
-        """ appends a flag in the index of items indicating that the parent has sub items """
-        detailed_index = []
-        all_item_index = self.get_index()
-        all_item_text = "\n".join(item_info[1] for item_info in all_item_index)
-        for fullname, relname, contenttype in index:
-            hassubitem = False
-            subitem_name_re = u"^{0}/[^/]+$".format(re.escape(relname))
-            regex = re.compile(subitem_name_re, re.UNICODE|re.M)
-            if regex.search(all_item_text):
-                hassubitem = True
-            detailed_index.append((fullname, relname, contenttype, hassubitem))
-        return detailed_index
-
-    def name_initial(self, names=None):
-        initials = [(name[1][0])
-                   for name in names]
+    def name_initial(self, subitems):
+        prefixlen = len(self.subitems_prefix)
+        initials = [(item.meta[NAME][prefixlen]) for item in subitems]  # XXX BROKEN - this is a list of names now
         return initials
 
     delete_template = 'delete.html'
@@ -651,922 +576,155 @@
     rename_template = 'rename.html'
     revert_template = 'revert.html'
 
-class NonExistent(Item):
-    def do_get(self, force_attachment=False, mimetype=None):
-        abort(404)
-
-    def _convert(self, doc):
-        abort(404)
-
-    def do_modify(self, contenttype, template_name):
-        # First, check if the current user has the required privileges
-        if not flaskg.user.may.create(self.name):
-            abort(403)
 
-        # XXX think about and add item template support
-        return render_template('modify_show_type_selection.html',
-                               item_name=self.name,
-                               contenttype_groups=CONTENTTYPE_GROUPS,
-                              )
-
-item_registry.register(NonExistent._factory, Type('application/x-nonexistent'))
-
-class ValidJSON(Validator):
-    """Validator for JSON
+class Contentful(Item):
     """
-    invalid_json_msg = L_('Invalid JSON.')
-
-    def validate(self, element, state):
-        try:
-            json.loads(element.value)
-        except:
-            return self.note_error(element, state, 'invalid_json_msg')
-        return True
+    Base class for Item subclasses that have content.
+    """
+    @property
+    def ModifyForm(self):
+        class C(self._ModifyForm):
+            content_form = self.content.ModifyForm
+        C.__name__ = 'ModifyForm'
+        return C
 
 
-class Binary(Item):
-    """ An arbitrary binary item, fallback class for every item mimetype. """
-    modify_help = """\
-There is no help, you're doomed!
-"""
-
-    template = "modify_binary.html"
-
-    # XXX reads item rev data into memory!
-    def get_data(self):
-        if self.rev is not None:
-            return self.rev.data.read()
-        else:
-            return ''
-    data = property(fget=get_data)
-
-    def _render_meta(self):
-        return "<pre>{0}</pre>".format(escape(self.meta_dict_to_text(self.meta, use_filter=False)))
+@register
+class Default(Contentful):
+    """
+    A "conventional" wiki item.
+    """
+    itemtype = u'default'
+    display_name = L_('Default')
+    description = L_('Wiki item')
+    order = -10
 
-    def get_templates(self, contenttype=None):
-        """ create a list of templates (for some specific contenttype) """
-        terms = [Term(WIKINAME, app.cfg.interwikiname), Term(TAGS, u'template')]
-        if contenttype is not None:
-            terms.append(Term(CONTENTTYPE, contenttype))
-        query = And(terms)
-        revs = flaskg.storage.search(query, sortedby=NAME_EXACT, limit=None)
-        return [rev.name for rev in revs]
+    def _do_modify_show_templates(self):
+        # call this if the item is still empty
+        rev_ids = []
+        item_templates = self.content.get_templates(self.contenttype)
+        return render_template('modify_select_template.html',
+                               item_name=self.name,
+                               itemtype=self.itemtype,
+                               rev=self.rev,
+                               contenttype=self.contenttype,
+                               templates=item_templates,
+                               first_rev_id=rev_ids and rev_ids[0],
+                               last_rev_id=rev_ids and rev_ids[-1],
+                               meta_rendered='',
+                               data_rendered='',
+                               )
 
-    def do_modify(self, contenttype, template_name):
-        # XXX think about and add item template support
-        #if template_name is None and isinstance(self.rev, DummyRev):
-        #    return self._do_modify_show_templates()
-        from MoinMoin.apps.frontend.views import CommentForm
-        class ModifyForm(CommentForm):
-            meta_text = String.using(optional=False).with_properties(placeholder=L_("MetaData (JSON)")).validated_by(ValidJSON())
-            data_file = FileStorage.using(optional=True, label=L_('Upload file:'))
+    def do_show(self, revid):
+        show_revision = revid != CURRENT
+        show_navigation = False # TODO
+        first_rev = last_rev = None # TODO
+        return render_template(self.show_template,
+                               item=self, item_name=self.name,
+                               rev=self.rev,
+                               contenttype=self.contenttype,
+                               first_rev_id=first_rev,
+                               last_rev_id=last_rev,
+                               data_rendered=Markup(self.content._render_data()),
+                               show_revision=show_revision,
+                               show_navigation=show_navigation,
+                              )
 
-        if request.method == 'GET':
-            form = ModifyForm.from_defaults()
-            TextCha(form).amend_form()
-            form['meta_text'] = self.meta_dict_to_text(self.prepare_meta_for_modify(self.meta))
-        elif request.method == 'POST':
-            form = ModifyForm.from_flat(request.form.items() + request.files.items())
-            TextCha(form).amend_form()
+    def do_modify(self):
+        method = request.method
+        if method == 'GET':
+            if isinstance(self.content, NonExistentContent):
+                return render_template('modify_select_contenttype.html',
+                                       item_name=self.name,
+                                       itemtype=self.itemtype,
+                                       group_names=content_registry.group_names,
+                                       groups=content_registry.groups,
+                                      )
+            item = self
+            if isinstance(self.rev, DummyRev):
+                template_name = request.values.get('template')
+                if template_name is None:
+                    return self._do_modify_show_templates()
+                elif template_name:
+                    item = Item.create(template_name)
+            form = self.ModifyForm.from_item(item)
+        elif method == 'POST':
+            # XXX workaround for *Draw items
+            if isinstance(self.content, Draw):
+                try:
+                    self.content.handle_post()
+                except AccessDenied:
+                    abort(403)
+                else:
+                    # *Draw Applets POSTs more than once, redirecting would
+                    # break them
+                    return "OK"
+            form = self.ModifyForm.from_request(request)
             if form.validate():
+                meta, data, contenttype_guessed, comment = form._dump(self)
+                contenttype_qs = request.values.get('contenttype')
                 try:
-                    self.modify() # XXX
+                    self.modify(meta, data, comment, contenttype_guessed, contenttype_qs)
                 except AccessDenied:
                     abort(403)
                 else:
                     return redirect(url_for_item(self.name))
-        return render_template(self.template,
+        return render_template(self.modify_template,
                                item_name=self.name,
                                rows_meta=str(ROWS_META), cols=str(COLS),
-                               help=self.modify_help,
                                form=form,
                                search_form=None,
                               )
 
-    def _render_data_diff(self, oldrev, newrev):
-        hash_name = HASH_ALGORITHM
-        if oldrev.meta[hash_name] == newrev.meta[hash_name]:
-            return _("The items have the same data hash code (that means they very likely have the same data).")
-        else:
-            return _("The items have different data.")
-
-    _render_data_diff_text = _render_data_diff
-    _render_data_diff_raw = _render_data_diff
-
-    def _render_data_diff_atom(self, oldrev, newrev):
-        return render_template('atom.html',
-                               oldrev=oldrev, newrev=newrev, get='binary',
-                               content=Markup(self._render_data()))
-
-    def _convert(self, doc):
-        return _("Impossible to convert the data to the contenttype: %(contenttype)s",
-                 contenttype=request.values.get('contenttype'))
-
-    def do_get(self, force_attachment=False, mimetype=None):
-        hash = self.rev.meta.get(HASH_ALGORITHM)
-        if is_resource_modified(request.environ, hash): # use hash as etag
-            return self._do_get_modified(hash, force_attachment=force_attachment, mimetype=mimetype)
-        else:
-            return Response(status=304)
-
-    def _do_get_modified(self, hash, force_attachment=False, mimetype=None):
-        member = request.values.get('member')
-        return self._do_get(hash, member, force_attachment=force_attachment, mimetype=mimetype)
-
-    def _do_get(self, hash, member=None, force_attachment=False, mimetype=None):
-        if member: # content = file contained within a archive item revision
-            path, filename = os.path.split(member)
-            mt = MimeType(filename=filename)
-            content_length = None
-            file_to_send = self.get_member(member)
-        else: # content = item revision
-            rev = self.rev
-            filename = rev.item.name
-            try:
-                mimestr = rev.meta[CONTENTTYPE]
-            except KeyError:
-                mt = MimeType(filename=filename)
-            else:
-                mt = MimeType(mimestr=mimestr)
-            content_length = rev.meta[SIZE]
-            file_to_send = rev.data
-        if mimetype:
-            content_type = mimetype
-        else:
-            content_type = mt.content_type()
-        as_attachment = force_attachment or mt.as_attachment(app.cfg)
-        return send_file(file=file_to_send,
-                         mimetype=content_type,
-                         as_attachment=as_attachment, attachment_filename=filename,
-                         cache_timeout=10, # wiki data can change rapidly
-                         add_etags=True, etag=hash, conditional=True)
-
-item_registry.register(Binary._factory, Type('*/*'))
-
-
-class RenderableBinary(Binary):
-    """ Base class for some binary stuff that renders with a object tag. """
-
-
-class Application(Binary):
-    """ Base class for application/* """
-
-
-class TarMixin(object):
-    """
-    TarMixin offers additional functionality for tar-like items to list and
-    access member files and to create new revisions by multiple posts.
-    """
-    def list_members(self):
-        """
-        list tar file contents (member file names)
-        """
-        self.rev.data.seek(0)
-        tf = tarfile.open(fileobj=self.rev.data, mode='r')
-        return tf.getnames()
-
-    def get_member(self, name):
-        """
-        return a file-like object with the member file data
-
-        :param name: name of the data in the container file
-        """
-        self.rev.data.seek(0)
-        tf = tarfile.open(fileobj=self.rev.data, mode='r')
-        return tf.extractfile(name)
-
-    def put_member(self, name, content, content_length, expected_members):
-        """
-        puts a new member file into a temporary tar container.
-        If all expected members have been put, it saves the tar container
-        to a new item revision.
-
-        :param name: name of the data in the container file
-        :param content: the data to store into the tar file (str or file-like)
-        :param content_length: byte-length of content (for str, None can be given)
-        :param expected_members: set of expected member file names
-        """
-        if not name in expected_members:
-            raise StorageError("tried to add unexpected member {0!r} to container item {1!r}".format(name, self.name))
-        if isinstance(name, unicode):
-            name = name.encode('utf-8')
-        temp_fname = os.path.join(tempfile.gettempdir(), 'TarContainer_' +
-                                  cache_key(usage='TarContainer', name=self.name))
-        tf = tarfile.TarFile(temp_fname, mode='a')
-        ti = tarfile.TarInfo(name)
-        if isinstance(content, str):
-            if content_length is None:
-                content_length = len(content)
-            content = StringIO(content) # we need a file obj
-        elif not hasattr(content, 'read'):
-            logging.error("unsupported content object: {0!r}".format(content))
-            raise StorageError("unsupported content object: {0!r}".format(content))
-        assert content_length >= 0  # we don't want -1 interpreted as 4G-1
-        ti.size = content_length
-        tf.addfile(ti, content)
-        tf_members = set(tf.getnames())
-        tf.close()
-        if tf_members - expected_members:
-            msg = "found unexpected members in container item {0!r}".format(self.name)
-            logging.error(msg)
-            os.remove(temp_fname)
-            raise StorageError(msg)
-        if tf_members == expected_members:
-            # everything we expected has been added to the tar file, save the container as revision
-            meta = {CONTENTTYPE: self.contenttype}
-            data = open(temp_fname, 'rb')
-            self._save(meta, data, name=self.name, action=u'SAVE', comment='')
-            data.close()
-            os.remove(temp_fname)
-
-
-class ApplicationXTar(TarMixin, Application):
-    """
-    Tar items
-    """
-
-item_registry.register(ApplicationXTar._factory, Type('application/x-tar'))
-item_registry.register(ApplicationXTar._factory, Type('application/x-gtar'))
-
-
-class ZipMixin(object):
-    """
-    ZipMixin offers additional functionality for zip-like items to list and
-    access member files.
-    """
-    def list_members(self):
-        """
-        list zip file contents (member file names)
-        """
-        self.rev.data.seek(0)
-        zf = zipfile.ZipFile(self.rev.data, mode='r')
-        return zf.namelist()
-
-    def get_member(self, name):
-        """
-        return a file-like object with the member file data
-
-        :param name: name of the data in the zip file
-        """
-        self.rev.data.seek(0)
-        zf = zipfile.ZipFile(self.rev.data, mode='r')
-        return zf.open(name, mode='r')
-
-    def put_member(self, name, content, content_length, expected_members):
-        raise NotImplementedError
-
-
-class ApplicationZip(ZipMixin, Application):
-    """
-    Zip items
-    """
-
-item_registry.register(ApplicationZip._factory, Type('application/zip'))
-
-
-class PDF(Application):
-    """ PDF """
-
-item_registry.register(PDF._factory, Type('application/pdf'))
-
-
-class Video(Binary):
-    """ Base class for video/* """
-
-item_registry.register(Video._factory, Type('video/*'))
-
-
-class Audio(Binary):
-    """ Base class for audio/* """
-
-item_registry.register(Audio._factory, Type('audio/*'))
-
-
-class Image(Binary):
-    """ Base class for image/* """
-
-item_registry.register(Image._factory, Type('image/*'))
-
-
-class RenderableImage(RenderableBinary):
-    """ Base class for renderable Image mimetypes """
-
-
-class SvgImage(RenderableImage):
-    """ SVG images use <object> tag mechanism from RenderableBinary base class """
-
-item_registry.register(SvgImage._factory, Type('image/svg+xml'))
-
-
-class RenderableBitmapImage(RenderableImage):
-    """ PNG/JPEG/GIF images use <img> tag (better browser support than <object>) """
-    # if mimetype is also transformable, please register in TransformableImage ONLY!
-
-
-class TransformableBitmapImage(RenderableBitmapImage):
-    """ We can transform (resize, rotate, mirror) some image types """
-    def _transform(self, content_type, size=None, transpose_op=None):
-        """ resize to new size (optional), transpose according to exif infos,
-            result data should be content_type.
-        """
-        try:
-            from PIL import Image as PILImage
-        except ImportError:
-            # no PIL, we can't do anything, we just output the revision data as is
-            return content_type, self.rev.data.read()
-
-        if content_type == 'image/jpeg':
-            output_type = 'JPEG'
-        elif content_type == 'image/png':
-            output_type = 'PNG'
-        elif content_type == 'image/gif':
-            output_type = 'GIF'
-        else:
-            raise ValueError("content_type {0!r} not supported".format(content_type))
-
-        # revision obj has read() seek() tell(), thus this works:
-        image = PILImage.open(self.rev.data)
-        image.load()
-
-        try:
-            # if we have EXIF data, we can transpose (e.g. rotate left),
-            # so the rendered image is correctly oriented:
-            transpose_op = transpose_op or 1 # or self.exif['Orientation']
-        except KeyError:
-            transpose_op = 1 # no change
-
-        if size is not None:
-            image = image.copy() # create copy first as thumbnail works in-place
-            image.thumbnail(size, PILImage.ANTIALIAS)
-
-        transpose_func = {
-            1: lambda image: image,
-            2: lambda image: image.transpose(PILImage.FLIP_LEFT_RIGHT),
-            3: lambda image: image.transpose(PILImage.ROTATE_180),
-            4: lambda image: image.transpose(PILImage.FLIP_TOP_BOTTOM),
-            5: lambda image: image.transpose(PILImage.ROTATE_90).transpose(PILImage.FLIP_TOP_BOTTOM),
-            6: lambda image: image.transpose(PILImage.ROTATE_270),
-            7: lambda image: image.transpose(PILImage.ROTATE_90).transpose(PILImage.FLIP_LEFT_RIGHT),
-            8: lambda image: image.transpose(PILImage.ROTATE_90),
-        }
-        image = transpose_func[transpose_op](image)
-
-        outfile = StringIO()
-        image.save(outfile, output_type)
-        data = outfile.getvalue()
-        outfile.close()
-        return content_type, data
-
-    def _do_get_modified(self, hash, force_attachment=False, mimetype=None):
-        try:
-            width = int(request.values.get('w'))
-        except (TypeError, ValueError):
-            width = None
-        try:
-            height = int(request.values.get('h'))
-        except (TypeError, ValueError):
-            height = None
-        try:
-            transpose = int(request.values.get('t'))
-            assert 1 <= transpose <= 8
-        except (TypeError, ValueError, AssertionError):
-            transpose = 1
-        if width or height or transpose != 1:
-            # resize requested, XXX check ACL behaviour! XXX
-            hash_name = HASH_ALGORITHM
-            hash_hexdigest = self.rev.meta[hash_name]
-            cid = cache_key(usage="ImageTransform",
-                            hash_name=hash_name,
-                            hash_hexdigest=hash_hexdigest,
-                            width=width, height=height, transpose=transpose)
-            c = app.cache.get(cid)
-            if c is None:
-                if mimetype:
-                    content_type = mimetype
-                else:
-                    content_type = self.rev.meta[CONTENTTYPE]
-                size = (width or 99999, height or 99999)
-                content_type, data = self._transform(content_type, size=size, transpose_op=transpose)
-                headers = wikiutil.file_headers(content_type=content_type, content_length=len(data))
-                app.cache.set(cid, (headers, data))
-            else:
-                # XXX TODO check ACL behaviour
-                headers, data = c
-            return Response(data, headers=headers)
-        else:
-            return self._do_get(hash, force_attachment=force_attachment, mimetype=mimetype)
-
-    def _render_data_diff_atom(self, oldrev, newrev):
-        if PIL is None:
-            # no PIL, we can't do anything, we just call the base class method
-            return super(TransformableBitmapImage, self)._render_data_diff_atom(oldrev, newrev)
-        url = url_for('frontend.diffraw', _external=True, item_name=self.name, rev1=oldrev.revid, rev2=newrev.revid)
-        return render_template('atom.html',
-                               oldrev=oldrev, newrev=newrev, get='binary',
-                               content=Markup('<img src="{0}" />'.format(escape(url))))
-
-    def _render_data_diff(self, oldrev, newrev):
-        if PIL is None:
-            # no PIL, we can't do anything, we just call the base class method
-            return super(TransformableBitmapImage, self)._render_data_diff(oldrev, newrev)
-        url = url_for('frontend.diffraw', item_name=self.name, rev1=oldrev.revid, rev2=newrev.revid)
-        return Markup('<img src="{0}" />'.format(escape(url)))
-
-    def _render_data_diff_raw(self, oldrev, newrev):
-        hash_name = HASH_ALGORITHM
-        cid = cache_key(usage="ImageDiff",
-                        hash_name=hash_name,
-                        hash_old=oldrev.meta[hash_name],
-                        hash_new=newrev.meta[hash_name])
-        c = app.cache.get(cid)
-        if c is None:
-            if PIL is None:
-                abort(404) # TODO render user friendly error image
-
-            content_type = newrev.meta[CONTENTTYPE]
-            if content_type == 'image/jpeg':
-                output_type = 'JPEG'
-            elif content_type == 'image/png':
-                output_type = 'PNG'
-            elif content_type == 'image/gif':
-                output_type = 'GIF'
-            else:
-                raise ValueError("content_type {0!r} not supported".format(content_type))
-
-            try:
-                oldimage = PILImage.open(oldrev.data)
-                newimage = PILImage.open(newrev.data)
-                oldimage.load()
-                newimage.load()
-                diffimage = PILdiff(newimage, oldimage)
-                outfile = StringIO()
-                diffimage.save(outfile, output_type)
-                data = outfile.getvalue()
-                outfile.close()
-                headers = wikiutil.file_headers(content_type=content_type, content_length=len(data))
-                app.cache.set(cid, (headers, data))
-            except (IOError, ValueError) as err:
-                logging.exception("error during PILdiff: {0}".format(err.message))
-                abort(404) # TODO render user friendly error image
-        else:
-            # XXX TODO check ACL behaviour
-            headers, data = c
-        return Response(data, headers=headers)
-
-    def _render_data_diff_text(self, oldrev, newrev):
-        return super(TransformableBitmapImage, self)._render_data_diff_text(oldrev, newrev)
-
-item_registry.register(TransformableBitmapImage._factory, Type('image/png'))
-item_registry.register(TransformableBitmapImage._factory, Type('image/jpeg'))
-item_registry.register(TransformableBitmapImage._factory, Type('image/gif'))
+    show_template = 'show.html'
+    modify_template = 'modify.html'
 
 
-class Text(Binary):
-    """ Base class for text/* """
-    template = "modify_text.html"
-
-    # text/plain mandates crlf - but in memory, we want lf only
-    def data_internal_to_form(self, text):
-        """ convert data from memory format to form format """
-        return text.replace(u'\n', u'\r\n')
-
-    def data_form_to_internal(self, data):
-        """ convert data from form format to memory format """
-        return data.replace(u'\r\n', u'\n')
-
-    def data_internal_to_storage(self, text):
-        """ convert data from memory format to storage format """
-        return text.replace(u'\n', u'\r\n').encode(config.charset)
-
-    def data_storage_to_internal(self, data):
-        """ convert data from storage format to memory format """
-        return data.decode(config.charset).replace(u'\r\n', u'\n')
-
-    def _get_data_diff_html(self, oldrev, newrev, template):
-        from MoinMoin.util.diff_html import diff
-        old_text = self.data_storage_to_internal(oldrev.data.read())
-        new_text = self.data_storage_to_internal(newrev.data.read())
-        storage_item = flaskg.storage[self.name]
-        diffs = [(d[0], Markup(d[1]), d[2], Markup(d[3])) for d in diff(old_text, new_text)]
-        return render_template(template,
-                               item_name=self.name,
-                               oldrev=oldrev,
-                               newrev=newrev,
-                               diffs=diffs,
-                               )
-
-    def _render_data_diff_atom(self, oldrev, newrev):
-        """ renders diff in HTML for atom feed """
-        return self._get_data_diff_html(oldrev, newrev, 'diff_text_atom.html')
-
-    def _render_data_diff(self, oldrev, newrev):
-        return self._get_data_diff_html(oldrev, newrev, 'diff_text.html')
-
-    def _render_data_diff_text(self, oldrev, newrev):
-        from MoinMoin.util import diff_text
-        oldlines = self.data_storage_to_internal(oldrev.data.read()).split('\n')
-        newlines = self.data_storage_to_internal(newrev.data.read()).split('\n')
-        difflines = diff_text.diff(oldlines, newlines)
-        return '\n'.join(difflines)
-
-    _render_data_diff_raw = _render_data_diff
-
-    def _render_data_highlight(self):
-        from MoinMoin.converter import default_registry as reg
-        data_text = self.data_storage_to_internal(self.data)
-        # TODO: use registry as soon as it is in there
-        from MoinMoin.converter.pygments_in import Converter as PygmentsConverter
-        pygments_conv = PygmentsConverter(contenttype=self.contenttype)
-        doc = pygments_conv(data_text)
-        # TODO: Real output format
-        html_conv = reg.get(type_moin_document, Type('application/x-xhtml-moin-page'))
-        doc = html_conv(doc)
-        return conv_serialize(doc, {html.namespace: ''})
-
-    def do_modify(self, contenttype, template_name):
-        # XXX think about and add item template support
-        #if template_name is None and isinstance(self.rev, DummyRev):
-        #    return self._do_modify_show_templates()
-        from MoinMoin.apps.frontend.views import CommentForm
-        class ModifyForm(CommentForm):
-            meta_text = String.using(optional=False).with_properties(placeholder=L_("MetaData (JSON)")).validated_by(ValidJSON())
-            data_text = String.using(strip=False, optional=True).with_properties(placeholder=L_("Type your text here"))
-            data_file = FileStorage.using(optional=True, label=L_('Upload file:'))
-
-        if request.method == 'GET':
-            if template_name is None and isinstance(self.rev, DummyRev):
-                return self._do_modify_show_templates()
-            form = ModifyForm.from_defaults()
-            TextCha(form).amend_form()
-            if template_name:
-                item = Item.create(template_name)
-                form['data_text'] = self.data_storage_to_internal(item.data)
-            else:
-                form['data_text'] = self.data_storage_to_internal(self.data)
-            form['meta_text'] = self.meta_dict_to_text(self.prepare_meta_for_modify(self.meta))
-        elif request.method == 'POST':
-            form = ModifyForm.from_flat(request.form.items() + request.files.items())
-            TextCha(form).amend_form()
-            if form.validate():
-                try:
-                    self.modify() # XXX
-                except AccessDenied:
-                    abort(403)
-                else:
-                    return redirect(url_for_item(self.name))
-        return render_template(self.template,
-                               item_name=self.name,
-                               rows_data=str(ROWS_DATA), rows_meta=str(ROWS_META), cols=str(COLS),
-                               help=self.modify_help,
-                               form=form,
-                               search_form=None,
-                              )
-
-item_registry.register(Text._factory, Type('text/*'))
-
-
-class MarkupItem(Text):
+@register
+class Userprofile(Item):
     """
-    some kind of item with markup
-    (internal links and transcluded items)
-    """
-
-
-class MoinWiki(MarkupItem):
-    """ MoinMoin wiki markup """
-
-item_registry.register(MoinWiki._factory, Type('text/x.moin.wiki'))
-
-
-class CreoleWiki(MarkupItem):
-    """ Creole wiki markup """
-
-item_registry.register(CreoleWiki._factory, Type('text/x.moin.creole'))
-
-
-class MediaWiki(MarkupItem):
-    """ MediaWiki markup """
-
-item_registry.register(MediaWiki._factory, Type('text/x-mediawiki'))
-
-
-class ReST(MarkupItem):
-    """ ReStructured Text markup """
-
-item_registry.register(ReST._factory, Type('text/x-rst'))
-
-
-class HTML(Text):
-    """
-    HTML markup
-
-    Note: As we use html_in converter to convert this to DOM and later some
-          output converterter to produce output format (e.g. html_out for html
-          output), all(?) unsafe stuff will get lost.
-
-    Note: If raw revision data is accessed, unsafe stuff might be present!
+    Currently userprofile is implemented as a contenttype. This is a stub of an
+    itemtype implementation of userprofile.
     """
-    template = "modify_text_html.html"
-
-item_registry.register(HTML._factory, Type('text/html'))
-
-
-class DocBook(MarkupItem):
-    """ DocBook Document """
-    def _convert(self, doc):
-        from emeraldtree import ElementTree as ET
-        from MoinMoin.converter import default_registry as reg
-
-        doc = self._expand_document(doc)
-
-        # We convert the internal representation of the document
-        # into a DocBook document
-        conv = reg.get(type_moin_document, Type('application/docbook+xml'))
-
-        doc = conv(doc)
-
-        # We determine the different namespaces of the output form
-        output_namespaces = {
-             docbook.namespace: '',
-             xlink.namespace: 'xlink',
-         }
-
-        # We convert the result into a StringIO object
-        # With the appropriate namespace
-        # TODO: Some other operation should probably be done here too
-        # like adding a doctype
-        file_to_send = StringIO()
-        tree = ET.ElementTree(doc)
-        tree.write(file_to_send, namespaces=output_namespaces)
-
-        # We determine the different parameters for the reply
-        mt = MimeType(mimestr='application/docbook+xml;charset=utf-8')
-        content_type = mt.content_type()
-        as_attachment = mt.as_attachment(app.cfg)
-        # After creation of the StringIO, we are at the end of the file
-        # so position is the size the file.
-        # and then we should move it back at the beginning of the file
-        content_length = file_to_send.tell()
-        file_to_send.seek(0)
-        # Important: empty filename keeps flask from trying to autodetect filename,
-        # as this would not work for us, because our file's are not necessarily fs files.
-        return send_file(file=file_to_send,
-                         mimetype=content_type,
-                         as_attachment=as_attachment, attachment_filename=None,
-                         cache_timeout=10, # wiki data can change rapidly
-                         add_etags=False, etag=None, conditional=True)
-
-item_registry.register(DocBook._factory, Type('application/docbook+xml'))
+    itemtype = u'userprofile'
+    display_name = L_('User profile')
+    description = L_('User profile item (not implemented yet!)')
 
 
-class TWikiDraw(TarMixin, Image):
-    """
-    drawings by TWikiDraw applet. It creates three files which are stored as tar file.
+@register
+class NonExistent(Item):
     """
-    modify_help = ""
-    template = "modify_twikidraw.html"
-
-    def modify(self):
-        # called from modify UI/POST
-        file_upload = request.files.get('filepath')
-        filename = request.form['filename']
-        basepath, basename = os.path.split(filename)
-        basename, ext = os.path.splitext(basename)
-
-        filecontent = file_upload.stream
-        content_length = None
-        if ext == '.draw': # TWikiDraw POSTs this first
-            filecontent = filecontent.read() # read file completely into memory
-            filecontent = filecontent.replace("\r", "")
-        elif ext == '.map':
-            filecontent = filecontent.read() # read file completely into memory
-            filecontent = filecontent.strip()
-        elif ext == '.png':
-            #content_length = file_upload.content_length
-            # XXX gives -1 for wsgiref, gives 0 for werkzeug :(
-            # If this is fixed, we could use the file obj, without reading it into memory completely:
-            filecontent = filecontent.read()
-
-        self.put_member('drawing' + ext, filecontent, content_length,
-                        expected_members=set(['drawing.draw', 'drawing.map', 'drawing.png']))
-
-    def do_modify(self, contenttype, template_name):
-        # XXX think about and add item template support
-        #if template_name is None and isinstance(self.rev, DummyRev):
-        #    return self._do_modify_show_templates()
-        from MoinMoin.apps.frontend.views import CommentForm
-        class ModifyForm(CommentForm):
-            # XXX as the "saving" POSTs come from TWikiDraw (not the form), editing meta_text doesn't work
-            meta_text = String.using(optional=False).with_properties(placeholder=L_("MetaData (JSON)")).validated_by(ValidJSON())
-            data_file = FileStorage.using(optional=True, label=L_('Upload file:'))
+    A dummy Item for nonexistent items (when modifying, a nonexistent item with
+    undetermined itemtype)
+    """
+    itemtype = u'nonexistent'
+    shown = False
 
-        if request.method == 'GET':
-            form = ModifyForm.from_defaults()
-            TextCha(form).amend_form()
-            # XXX currently this is rather pointless, as the form does not get POSTed:
-            form['meta_text'] = self.meta_dict_to_text(self.prepare_meta_for_modify(self.meta))
-        elif request.method == 'POST':
-            # this POST comes directly from TWikiDraw (not from Browser), thus no validation
-            try:
-                self.modify() # XXX
-            except AccessDenied:
-                abort(403)
-            else:
-                # TWikiDraw POSTs more than once, redirecting would break them
-                return "OK"
-        return render_template(self.template,
-                               item_name=self.name,
-                               rows_meta=str(ROWS_META), cols=str(COLS),
-                               help=self.modify_help,
-                               form=form,
-                               search_form=None,
-                              )
+    def _convert(self, doc):
+        abort(404)
 
-    def _render_data(self):
-        # TODO: this could be a converter -> dom, then transcluding this kind
-        # of items and also rendering them with the code in base class could work
-        item_name = self.name
-        drawing_url = url_for('frontend.get_item', item_name=item_name, member='drawing.draw', rev=self.rev.revid)
-        png_url = url_for('frontend.get_item', item_name=item_name, member='drawing.png', rev=self.rev.revid)
-        title = _('Edit drawing %(filename)s (opens in new window)', filename=item_name)
+    def do_show(self, revid):
+        # First, check if the current user has the required privileges
+        if flaskg.user.may.create(self.name):
+            content = self._select_itemtype()
+        else:
+            content = render_template('show_nonexistent.html',
+                                      item_name=self.name,
+                                     )
+        return Response(content, 404)
 
-        mapfile = self.get_member('drawing.map')
-        try:
-            image_map = mapfile.read()
-            mapfile.close()
-        except (IOError, OSError):
-            image_map = ''
-        if image_map:
-            # we have a image map. inline it and add a map ref to the img tag
-            mapid = 'ImageMapOf' + item_name
-            image_map = image_map.replace('%MAPNAME%', mapid)
-            # add alt and title tags to areas
-            image_map = re.sub(r'href\s*=\s*"((?!%TWIKIDRAW%).+?)"', r'href="\1" alt="\1" title="\1"', image_map)
-            image_map = image_map.replace('%TWIKIDRAW%"', '{0}" alt="{1}" title="{2}"'.format((drawing_url, title, title)))
-            title = _('Clickable drawing: %(filename)s', filename=item_name)
+    def do_modify(self):
+        # First, check if the current user has the required privileges
+        if not flaskg.user.may.create(self.name):
+            abort(403)
+        return self._select_itemtype()
 
-            return Markup(image_map + '<img src="{0}" alt="{1}" usemap="#{2}" />'.format(png_url, title, mapid))
-        else:
-            return Markup('<img src="{0}" alt="{1}" />'.format(png_url, title))
-
-item_registry.register(TWikiDraw._factory, Type('application/x-twikidraw'))
+    def _select_itemtype(self):
+        return render_template('modify_select_itemtype.html',
+                               item_name=self.name,
+                               itemtypes=item_registry.shown_entries,
+                              )
 
 
-class AnyWikiDraw(TarMixin, Image):
-    """
-    drawings by AnyWikiDraw applet. It creates three files which are stored as tar file.
-    """
-    modify_help = ""
-    template = "modify_anywikidraw.html"
-
-    def modify(self):
-        # called from modify UI/POST
-        file_upload = request.files.get('filepath')
-        filename = request.form['filename']
-        basepath, basename = os.path.split(filename)
-        basename, ext = os.path.splitext(basename)
-        filecontent = file_upload.stream
-        content_length = None
-        if ext == '.svg':
-            filecontent = filecontent.read() # read file completely into memory
-            filecontent = filecontent.replace("\r", "")
-        elif ext == '.map':
-            filecontent = filecontent.read() # read file completely into memory
-            filecontent = filecontent.strip()
-        elif ext == '.png':
-            #content_length = file_upload.content_length
-            # XXX gives -1 for wsgiref, gives 0 for werkzeug :(
-            # If this is fixed, we could use the file obj, without reading it into memory completely:
-            filecontent = filecontent.read()
-        self.put_member('drawing' + ext, filecontent, content_length,
-                        expected_members=set(['drawing.svg', 'drawing.map', 'drawing.png']))
-
-    def do_modify(self, contenttype, template_name):
-        # XXX think about and add item template support
-        #if template_name is None and isinstance(self.rev, DummyRev):
-        #    return self._do_modify_show_templates()
-        from MoinMoin.apps.frontend.views import CommentForm
-        class ModifyForm(CommentForm):
-            # XXX as the "saving" POSTs come from AnyWikiDraw (not the form), editing meta_text doesn't work
-            meta_text = String.using(optional=False).with_properties(placeholder=L_("MetaData (JSON)")).validated_by(ValidJSON())
-            data_file = FileStorage.using(optional=True, label=L_('Upload file:'))
-
-        if request.method == 'GET':
-            form = ModifyForm.from_defaults()
-            TextCha(form).amend_form()
-            # XXX currently this is rather pointless, as the form does not get POSTed:
-            form['meta_text'] = self.meta_dict_to_text(self.prepare_meta_for_modify(self.meta))
-        elif request.method == 'POST':
-            # this POST comes directly from AnyWikiDraw (not from Browser), thus no validation
-            try:
-                self.modify() # XXX
-            except AccessDenied:
-                abort(403)
-            else:
-                # AnyWikiDraw POSTs more than once, redirecting would break them
-                return "OK"
-        try:
-            drawing_exists = 'drawing.svg' in self.list_members()
-        except:
-            drawing_exists = False
-        return render_template(self.template,
-                               item_name=self.name,
-                               rows_meta=str(ROWS_META), cols=str(COLS),
-                               help=self.modify_help,
-                               drawing_exists=drawing_exists,
-                               form=form,
-                               search_form=None,
-                              )
-
-    def _render_data(self):
-        # TODO: this could be a converter -> dom, then transcluding this kind
-        # of items and also rendering them with the code in base class could work
-        item_name = self.name
-        drawing_url = url_for('frontend.get_item', item_name=item_name, member='drawing.svg', rev=self.rev.revid)
-        png_url = url_for('frontend.get_item', item_name=item_name, member='drawing.png', rev=self.rev.revid)
-        title = _('Edit drawing %(filename)s (opens in new window)', filename=self.name)
-
-        mapfile = self.get_member('drawing.map')
-        try:
-            image_map = mapfile.read()
-            mapfile.close()
-        except (IOError, OSError):
-            image_map = ''
-        if image_map:
-            # ToDo mapid must become uniq
-            # we have a image map. inline it and add a map ref to the img tag
-            # we have also to set a unique ID
-            mapid = 'ImageMapOf' + self.name
-            image_map = image_map.replace(u'id="drawing.svg"', '')
-            image_map = image_map.replace(u'name="drawing.svg"', u'name="{0}"'.format(mapid))
-            # unxml, because 4.01 concrete will not validate />
-            image_map = image_map.replace(u'/>', u'>')
-            title = _('Clickable drawing: %(filename)s', filename=self.name)
-            return Markup(image_map + '<img src="{0}" alt="{1}" usemap="#{2}" />'.format(png_url, title, mapid))
-        else:
-            return Markup('<img src="{0}" alt="{1}" />'.format(png_url, title))
-
-item_registry.register(AnyWikiDraw._factory, Type('application/x-anywikidraw'))
-
-
-class SvgDraw(TarMixin, Image):
-    """ drawings by svg-edit. It creates two files (svg, png) which are stored as tar file. """
-    modify_help = ""
-    template = "modify_svg-edit.html"
-
-    def modify(self):
-        # called from modify UI/POST
-        png_upload = request.values.get('png_data')
-        svg_upload = request.values.get('filepath')
-        filename = request.form['filename']
-        png_content = png_upload.decode('base_64')
-        png_content = base64.urlsafe_b64decode(png_content.split(',')[1])
-        svg_content = svg_upload.decode('base_64')
-        content_length = None
-        self.put_member("drawing.svg", svg_content, content_length,
-                        expected_members=set(['drawing.svg', 'drawing.png']))
-        self.put_member("drawing.png", png_content, content_length,
-                        expected_members=set(['drawing.svg', 'drawing.png']))
-
-    def do_modify(self, contenttype, template_name):
-        # XXX think about and add item template support
-        #if template_name is None and isinstance(self.rev, DummyRev):
-        #    return self._do_modify_show_templates()
-        from MoinMoin.apps.frontend.views import CommentForm
-        class ModifyForm(CommentForm):
-            # XXX as the "saving" POSTs come from SvgDraw (not the form), editing meta_text doesn't work
-            meta_text = String.using(optional=False).with_properties(placeholder=L_("MetaData (JSON)")).validated_by(ValidJSON())
-            data_file = FileStorage.using(optional=True, label=L_('Upload file:'))
-
-        if request.method == 'GET':
-            form = ModifyForm.from_defaults()
-            TextCha(form).amend_form()
-            # XXX currently this is rather pointless, as the form does not get POSTed:
-            form['meta_text'] = self.meta_dict_to_text(self.prepare_meta_for_modify(self.meta))
-        elif request.method == 'POST':
-            # this POST comes directly from SvgDraw (not from Browser), thus no validation
-            try:
-                self.modify() # XXX
-            except AccessDenied:
-                abort(403)
-            else:
-                # SvgDraw POSTs more than once, redirecting would break them
-                return "OK"
-        return render_template(self.template,
-                               item_name=self.name,
-                               rows_meta=str(ROWS_META), cols=str(COLS),
-                               help=self.modify_help,
-                               form=form,
-                               search_form=None,
-                              )
-
-    def _render_data(self):
-        # TODO: this could be a converter -> dom, then transcluding this kind
-        # of items and also rendering them with the code in base class could work
-        item_name = self.name
-        drawing_url = url_for('frontend.get_item', item_name=item_name, member='drawing.svg', rev=self.rev.revid)
-        png_url = url_for('frontend.get_item', item_name=item_name, member='drawing.png', rev=self.rev.revid)
-        return Markup('<img src="{0}" alt="{1}" />'.format(png_url, drawing_url))
-
-item_registry.register(SvgDraw._factory, Type('application/x-svgdraw'))
-
+from ..util.pysupport import load_package_modules
+load_package_modules(__name__, __path__)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/MoinMoin/items/_tests/test_Blog.py	Sat Oct 06 17:46:24 2012 +0200
@@ -0,0 +1,170 @@
+# Copyright: 2012 MoinMoin:PavelSviderski
+# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
+
+"""
+    MoinMoin - MoinMoin.items.blog Tests
+"""
+
+import re
+
+from flask import url_for
+
+from MoinMoin._tests import become_trusted, update_item
+from MoinMoin.items import Item
+from MoinMoin.config import CONTENTTYPE, ITEMTYPE, PTIME, ACL, TAGS
+from MoinMoin.items.blog import ITEMTYPE_BLOG, ITEMTYPE_BLOG_ENTRY
+from MoinMoin.items.blog import Blog, BlogEntry
+
+
+class TestView(object):
+    def _test_view(self, item_name, req_args={}, data_tokens=[], exclude_data_tokens=[], regex=None):
+        with self.app.test_client() as c:
+            rv = c.get(url_for('frontend.show_item', item_name=item_name, **req_args))
+            for data in data_tokens:
+                assert data in rv.data
+            for data in exclude_data_tokens:
+                assert data not in rv.data
+            if regex:
+                assert regex.search(rv.data)
+
+
+class TestBlog(TestView):
+    NO_ENTRIES_MSG = u"There are no entries"
+
+    name = u'NewBlogItem'
+    contenttype = u'text/x.moin.wiki'
+    data = u"This is the header item of this blog"
+    meta = {CONTENTTYPE: contenttype, ITEMTYPE: ITEMTYPE_BLOG}
+    comment = u'saved it'
+    entries = [{'name': name + u'/NewBlogEntryItem1', 'data': u"First blog entry"},
+               {'name': name + u'/NewBlogEntryItem2', 'data': u"Second blog entry"},
+               {'name': name + u'/NewBlogEntryItem3', 'data': u"Third blog entry"},
+               {'name': name + u'/NewBlogEntryItem4', 'data': u"Fourth blog entry"}, ]
+    entry_meta = {CONTENTTYPE: contenttype, ITEMTYPE: ITEMTYPE_BLOG_ENTRY}
+
+    def _publish_entry(se