Replaced shebang line from python2.7 to python3.4.
[philipp/winterrodeln/wrpylib.git] / wrpylib / wrmwmarkup.py
index c9aba4ffa40c5de9957f9ed053fe85dbfe7dbbba..bc72a391f66d971142ba75ad3505b141a27a3990 100644 (file)
-#!/usr/bin/python2.7
+#!/usr/bin/python3.4
 # -*- coding: iso-8859-15 -*-
 # $Id$
 # $HeadURL$
-"""This module contains winterrodeln specific functions that are prcocessing the MediaWiki markup.
+"""This module contains winterrodeln specific functions that are processing the MediaWiki markup.
 """
 import re
 import xml.etree.ElementTree
+import collections
 import formencode
 import wrpylib.wrvalidators
 import wrpylib.mwmarkup
 
-WRMAP_POINT_TYPE = {'gasthaus': 'inn', 'haltestelle': 'busstop', 'parkplatz': 'carpark', 'achtung': 'attention', 'punkt': 'point'}
-WRMAP_LINE_TYPE = {'rodelbahn': 'sledrun', 'gehweg': 'walk', 'alternative': 'alternative', 'lift': 'lift', 'linie': 'line'}
+WRMAP_POINT_TYPES = ['gasthaus', 'haltestelle', 'parkplatz', 'achtung', 'foto', 'verleih', 'punkt']
+WRMAP_LINE_TYPES = ['rodelbahn', 'gehweg', 'alternative', 'lift', 'anfahrt', 'linie']
 
-def _conv(fnct, value, fieldname):
-    """Internal function.
-    Like one of the to_xxx functions (e.g. to_bool), but adds the field name to the error message"""
-    try: return fnct(value)
-    except formencode.Invalid as e: raise formencode.Invalid(u"Conversion error in field '%s': %s" % (fieldname, unicode(e)), e.value, e.state)
+
+class ParseError(RuntimeError):
+    """Exception used by some of the functions"""
+    pass
+
+
+class RodelbahnboxDictConverter(formencode.Validator):
+    """Converts a dict with Rodelbahnbox properties to a Sledrun class. Does no validation."""
+
+    def to_python(self, value, state=None):
+        """value is a dict of properties. If state is an object with the attribute sledrun, this sledrun class will be populated or updated."""
+        props = value
+        if isinstance(state, object) and hasattr(state, 'sledrun'):
+            sledrun = state.sledrun
+        else:
+            class Sledrun(object):
+                pass
+            sledrun = Sledrun()
+        for k, v in props.items():
+            if   k == 'Position': sledrun.position_latitude, sledrun.position_longitude = v
+            elif k == 'Position oben': sledrun.top_latitude, sledrun.top_longitude = v
+            elif k == 'Höhe oben': sledrun.top_elevation = v
+            elif k == 'Position unten': sledrun.bottom_latitude, sledrun.bottom_longitude = v
+            elif k == 'Höhe unten': sledrun.bottom_elevation = v
+            elif k == 'Länge': sledrun.length = v
+            elif k == 'Schwierigkeit': sledrun.difficulty = v
+            elif k == 'Lawinen': sledrun.avalanches = v
+            elif k == 'Betreiber': sledrun.operator = v
+            elif k == 'Öffentliche Anreise': sledrun.public_transport = v
+            elif k == 'Aufstieg möglich': sledrun.walkup_possible = v
+            elif k == 'Aufstieg getrennt': sledrun.walkup_separate, sledrun.walkup_separate_comment = v
+            elif k == 'Gehzeit': sledrun.walkup_time = v
+            elif k == 'Aufstiegshilfe': sledrun.lift, sledrun.lift_details = v
+            elif k == 'Beleuchtungsanlage': sledrun.night_light, sledrun.night_light_comment = v
+            elif k == 'Beleuchtungstage': sledrun.night_light_days, sledrun.night_light_days_comment = v
+            elif k == 'Rodelverleih': sledrun.sled_rental, sledrun.sled_rental_comment = v
+            elif k == 'Gütesiegel': sledrun.cachet = v
+            elif k == 'Webauskunft': sledrun.information_web = v
+            elif k == 'Telefonauskunft': sledrun.information_phone = v
+            elif k == 'Bild': sledrun.image = v
+            elif k == 'In Übersichtskarte': sledrun.show_in_overview = v
+            elif k == 'Forumid': sledrun.forum_id = v
+        return sledrun
+
+    def from_python(self, value, state=None):
+        """Converts a sledrun class to a dict of Rodelbahnbox properties. value is a sledrun instance."""
+        sledrun = value
+        r = collections.OrderedDict()
+        r['Position'] = (sledrun.position_latitude, sledrun.position_longitude)
+        r['Position oben'] = (sledrun.top_latitude, sledrun.top_longitude)
+        r['Höhe oben'] = sledrun.top_elevation
+        r['Position unten'] = (sledrun.bottom_latitude, sledrun.bottom_longitude)
+        r['Höhe unten'] = sledrun.bottom_elevation
+        r['Länge'] = sledrun.length
+        r['Schwierigkeit'] = sledrun.difficulty
+        r['Lawinen'] = sledrun.avalanches
+        r['Betreiber'] = sledrun.operator
+        r['Öffentliche Anreise'] = sledrun.public_transport
+        r['Aufstieg möglich'] = sledrun.walkup_possible
+        r['Aufstieg getrennt'] = (sledrun.walkup_separate, sledrun.walkup_separate_comment)
+        r['Gehzeit'] = sledrun.walkup_time
+        r['Aufstiegshilfe'] = (sledrun.lift, sledrun.lift_details)
+        r['Beleuchtungsanlage'] = (sledrun.night_light, sledrun.night_light_comment)
+        r['Beleuchtungstage'] = (sledrun.night_light_days, sledrun.night_light_days_comment)
+        r['Rodelverleih'] = (sledrun.sled_rental, sledrun.sled_rental_comment)
+        r['Gütesiegel'] = sledrun.cachet
+        r['Webauskunft'] = sledrun.information_web
+        r['Telefonauskunft'] = sledrun.information_phone
+        r['Bild'] = sledrun.image
+        r['In Übersichtskarte'] = sledrun.show_in_overview
+        r['Forumid'] = sledrun.forum_id
+        return r
+
+
+class WinterrodelnTemplateDict(formencode.Validator):
+    """Private helper class for RodelbahnboxValidator or GasthausboxValidator"""
+    def __init__(self, template_title):
+        self.template_title = template_title
+
+    def to_python(self, value, state):
+        title, anonym_params, named_params = value
+        if title != self.template_title:
+            raise formencode.Invalid('Template title has to be "{}".'.format(self.template_title), value, state)
+        if len(anonym_params) > 0:
+            raise formencode.Invalid('No anonymous parameters are allowed in "{}".'.format(self.template_title), value, state)
+        return named_params
+
+    def from_python(self, value, state):
+        return self.template_title, [], value
+
+
+class RodelbahnboxValidator(wrpylib.wrvalidators.RodelbahnboxDictValidator):
+    def __init__(self):
+        wrpylib.wrvalidators.RodelbahnboxDictValidator.__init__(self)
+        self.pre_validators=[wrpylib.mwmarkup.TemplateValidator(as_table=True, as_table_keylen=20), WinterrodelnTemplateDict('Rodelbahnbox')]
+        self.chained_validators = [RodelbahnboxDictConverter()]
 
 
 def rodelbahnbox_to_sledrun(wikitext, sledrun=None):
@@ -25,167 +117,110 @@ def rodelbahnbox_to_sledrun(wikitext, sledrun=None):
     to a sledrun. sledrun may be an instance of WrSledrunCache or an "empty" class (object()) (default).
     Raises a formencode.Invalid exception if the format is not OK or the Rodelbahnbox is not found.
     :return: (start, end, sledrun) tuple of the Rodelbahnbox."""
+    # find Rodelbahnbox
+    start, end = wrpylib.mwmarkup.find_template(wikitext, 'Rodelbahnbox')
+    if start is None: raise formencode.Invalid("Rodelbahnbox nicht gefunden", wikitext, None)
+
+    # convert to sledrun
     if sledrun is None:
-        class Sledrun(object): pass
-        sledrun = Sledrun()
-
-    # match Rodelbahnbox
-    start, end = wrpylib.mwmarkup.find_template(wikitext, u'Rodelbahnbox')
-    if start is None: raise formencode.Invalid(u"Rodelbahnbox nicht gefunden", wikitext, None)
-    template_title, properties = wrpylib.mwmarkup.split_template(wikitext[start:end])
-    
-    # process properties
-    for key, value in properties.iteritems():
-        if   key == u'Position': sledrun.position_latitude, sledrun.position_longitude = _conv(wrpylib.wrvalidators.GeoNone().to_python, value, key) # '47.583333 N 15.75 E'
-        elif key == u'Position oben': sledrun.top_latitude, sledrun.top_longitude = _conv(wrpylib.wrvalidators.GeoNone().to_python, value, key) # '47.583333 N 15.75 E'
-        elif key == u'Höhe oben': sledrun.top_elevation = _conv(wrpylib.wrvalidators.UnsignedNone().to_python, value, key) # '2000'
-        elif key == u'Position unten': sledrun.bottom_latitude, sledrun.bottom_longitude = _conv(wrpylib.wrvalidators.GeoNone().to_python, value, key) # '47.583333 N 15.75 E'
-        elif key == u'Höhe unten': sledrun.bottom_elevation = _conv(wrpylib.wrvalidators.UnsignedNone().to_python, value, key) # '1200'
-        elif key == u'Länge': sledrun.length = _conv(wrpylib.wrvalidators.UnsignedNone().to_python, value, key) # 3500
-        elif key == u'Schwierigkeit': sledrun.difficulty = _conv(wrpylib.wrvalidators.GermanDifficulty().to_python, value, key) # 'mittel' elif key == u'Lawinen': sledrun.avalanches = _conv(wrpylib.wrvalidators.GermanAvalanches().to_python, value, key) # 'kaum'
-        elif key == u'Lawinen': sledrun.avalanches = _conv(wrpylib.wrvalidators.GermanAvalanches().to_python, value, key) # 'kaum'
-        elif key == u'Betreiber': sledrun.operator = _conv(wrpylib.wrvalidators.UnicodeNone().to_python, value, key) # 'Max Mustermann'
-        elif key == u'Öffentliche Anreise': sledrun.public_transport = _conv(wrpylib.wrvalidators.GermanPublicTransport().to_python, value, key) # 'Mittelmäßig'
-        elif key == u'Aufstieg möglich': sledrun.walkup_possible = _conv(wrpylib.wrvalidators.GermanBoolNone().to_python, value, key) # 'Ja'
-        elif key == u'Aufstieg getrennt': sledrun.walkup_separate, sledrun.walkup_separate_comment = _conv(wrpylib.wrvalidators.GermanTristateFloatComment().to_python, value, key) # 'Ja'
-        elif key == u'Gehzeit': sledrun.walkup_time = _conv(wrpylib.wrvalidators.UnsignedNone().to_python, value, key) # 90
-        elif key == u'Aufstiegshilfe': sledrun.lift, sledrun.lift_details = _conv(wrpylib.wrvalidators.GermanLift().to_python, value, key) # 'Gondel (unterer Teil)'
-        elif key == u'Beleuchtungsanlage': sledrun.night_light, sledrun.night_light_comment = _conv(wrpylib.wrvalidators.GermanTristateFloatComment().to_python, value, key)
-        elif key == u'Beleuchtungstage': sledrun.night_light_days, sledrun.night_light_days_comment = _conv(wrpylib.wrvalidators.UnsignedCommentNone(7).to_python, value, key) # '3 (Montag, Mittwoch, Freitag)'
-        elif key == u'Rodelverleih': sledrun.sled_rental, sledrun.sled_rental_comment = _conv(wrpylib.wrvalidators.SledRental().to_python, value, key) # 'Talstation Serlesbahnan'
-        elif key == u'Gütesiegel': sledrun.cachet = _conv(wrpylib.wrvalidators.GermanCachet().to_python, value, key) # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
-        elif key == u'Webauskunft': sledrun.information_web = _conv(wrpylib.wrvalidators.UrlNeinNone().to_python, value, key) # 'http://www.nösslachhütte.at/page9.php'
-        elif key == u'Telefonauskunft': sledrun.information_phone = _conv(wrpylib.wrvalidators.PhoneCommentListNeinLoopNone(comments_are_optional=False).to_python, value, key) # '+43-664-5487520 (Mitterer Alm)'
-        elif key == u'Bild': sledrun.image = _conv(wrpylib.wrvalidators.UnicodeNone().to_python, value, key)
-        elif key == u'In Übersichtskarte': sledrun.show_in_overview = _conv(wrpylib.wrvalidators.GermanBoolNone().to_python, value, key)
-        elif key == u'Forumid': sledrun.forum_id = _conv(wrpylib.wrvalidators.UnsignedNeinNone().to_python, value, key)
-        else: raise formencode.Invalid(u"Unbekannte Eigenschaft der Rodelbahnbox: '%s' (mit Wert '%s')" % (key, value), value, None)
-    return start, end, sledrun
-
-
-def sledrun_to_rodelbahnbox(sledrun, version):
+        state = None
+    else:
+        class State(object):
+            pass
+        state = State()
+        state.sledrun = sledrun
+    return start, end, RodelbahnboxValidator().to_python(wikitext[start:end], state)
+
+
+def sledrun_to_rodelbahnbox(sledrun, version=None):
     """Converts a sledrun class to the {{Rodelbahnbox}} representation.
     The sledrun class has to have properties like position_latitude, ...
     See the table sledruncache for field (column) values.
     :param sledrun: an arbitrary class that contains the right properties
     :param version: a string specifying the version of the rodelbahnbox zu produce.
-        Version '1.3' and '1.4' are supported."""
-    keys = []
-    values = []
-    keys.append(u'Position')
-    values.append(wrpylib.wrvalidators.GeoNone().from_python((sledrun.position_latitude, sledrun.position_longitude)))
-    keys.append(u'Position oben')
-    values.append(wrpylib.wrvalidators.GeoNone().from_python((sledrun.top_latitude, sledrun.top_longitude)))
-    keys.append(u'Höhe oben')
-    values.append(wrpylib.wrvalidators.UnsignedNone().from_python(sledrun.top_elevation))
-    keys.append(u'Position unten')
-    values.append(wrpylib.wrvalidators.GeoNone().from_python((sledrun.bottom_latitude, sledrun.bottom_longitude)))
-    keys.append(u'Höhe unten')
-    values.append(wrpylib.wrvalidators.UnsignedNone().from_python(sledrun.bottom_elevation))
-    keys.append(u'Länge')
-    values.append(wrpylib.wrvalidators.UnsignedNone().from_python(sledrun.length))
-    keys.append(u'Schwierigkeit')
-    values.append(wrpylib.wrvalidators.GermanDifficulty().from_python(sledrun.difficulty))
-    keys.append(u'Lawinen')
-    values.append(wrpylib.wrvalidators.GermanAvalanches().from_python(sledrun.avalanches))
-    keys.append(u'Betreiber')
-    values.append(wrpylib.wrvalidators.UnicodeNone().from_python(sledrun.operator))
-    keys.append(u'Öffentliche Anreise')
-    values.append(wrpylib.wrvalidators.GermanPublicTransport().from_python(sledrun.public_transport))
-    if version == '1.4':
-        keys.append(u'Aufstieg möglich')
-        values.append(wrpylib.wrvalidators.GermanBoolNone().from_python(sledrun.walkup_possible))
-    keys.append(u'Aufstieg getrennt')
-    values.append(wrpylib.wrvalidators.GermanTristateFloatComment().from_python((sledrun.walkup_separate, sledrun.walkup_separate_comment)))
-    keys.append(u'Gehzeit')
-    values.append(wrpylib.wrvalidators.UnsignedNone().from_python(sledrun.walkup_time))
-    keys.append(u'Aufstiegshilfe')
-    values.append(wrpylib.wrvalidators.GermanLift().from_python((sledrun.lift, sledrun.lift_details)))
-    keys.append(u'Beleuchtungsanlage')
-    values.append(wrpylib.wrvalidators.GermanTristateFloatComment().from_python((sledrun.night_light, sledrun.night_light_comment)))
-    keys.append(u'Beleuchtungstage')
-    values.append(wrpylib.wrvalidators.UnsignedCommentNone(max=7).from_python((sledrun.night_light_days, sledrun.night_light_days_comment)))
-    keys.append(u'Rodelverleih')
-    values.append(wrpylib.wrvalidators.SledRental().from_python((sledrun.sled_rental, sledrun.sled_rental_comment)))
-    keys.append(u'Gütesiegel')
-    values.append(wrpylib.wrvalidators.GermanCachet().from_python(sledrun.cachet))
-    keys.append(u'Webauskunft')
-    values.append(wrpylib.wrvalidators.UrlNeinNone().from_python(sledrun.information_web))
-    keys.append(u'Telefonauskunft')
-    values.append(wrpylib.wrvalidators.PhoneCommentListNeinLoopNone(comments_are_optional=False).from_python(sledrun.information_phone))
-    keys.append(u'Bild')
-    values.append(wrpylib.wrvalidators.UnicodeNone().from_python(sledrun.image))
-    keys.append(u'In Übersichtskarte')
-    values.append(wrpylib.wrvalidators.GermanBoolNone().from_python(sledrun.show_in_overview))
-    keys.append(u'Forumid')
-    values.append(wrpylib.wrvalidators.UnsignedNeinNone().from_python(sledrun.forum_id))
-    return wrpylib.mwmarkup.create_template(u'Rodelbahnbox', [], keys, values, True, 20)
+        Version '1.4' is supported."""
+    assert version in [None, '1.4']
+    return RodelbahnboxValidator().from_python(sledrun)
+
+
+class GasthausboxDictConverter(formencode.Validator):
+    """Converts a dict with Gasthausbox properties to a Inn class. Does no validation."""
+
+    def to_python(self, value, state=None):
+        """value is a dict of properties. If state is an object with the attribute inn, this inn class will be populated or updated."""
+        props = value
+        if isinstance(state, object) and hasattr(state, 'inn'):
+            inn = state.inn
+        else:
+            class Inn(object):
+                pass
+            inn = Inn()
+        for k, v in props.items():
+            if   k == 'Position': inn.position_latitude, inn.position_longitude = v
+            elif k == 'Höhe': inn.position_elevation = v
+            elif k == 'Betreiber': inn.operator = v
+            elif k == 'Sitzplätze': inn.seats = v
+            elif k == 'Übernachtung': inn.overnight, inn.overnight_comment = v
+            elif k == 'Rauchfrei': inn.nonsmoker_area, inn.smoker_area = v
+            elif k == 'Rodelverleih': inn.sled_rental, inn.sled_rental_comment = v
+            elif k == 'Handyempfang': inn.mobile_provider = v
+            elif k == 'Homepage': inn.homepage = v
+            elif k == 'E-Mail': inn.email_list = v
+            elif k == 'Telefon': inn.phone_list = v
+            elif k == 'Bild': inn.image = v
+            elif k == 'Rodelbahnen': inn.sledding_list = v
+        return inn
+
+    def from_python(self, value, state=None):
+        """Converts an inn class to a dict of Gasthausbox properties. value is an Inn instance."""
+        inn = value
+        r = collections.OrderedDict()
+        r['Position'] = (inn.position_latitude, inn.position_longitude)
+        r['Höhe'] = inn.position_elevation
+        r['Betreiber'] = inn.operator
+        r['Sitzplätze'] = inn.seats
+        r['Übernachtung'] = (inn.overnight, inn.overnight_comment)
+        r['Rauchfrei'] = (inn.nonsmoker_area, inn.smoker_area)
+        r['Rodelverleih'] = (inn.sled_rental, inn.sled_rental_comment)
+        r['Handyempfang'] = inn.mobile_provider
+        r['Homepage'] = inn.homepage
+        r['E-Mail'] = inn.email_list
+        r['Telefon'] = inn.phone_list
+        r['Bild'] = inn.image
+        r['Rodelbahnen'] = inn.sledding_list
+        return r
+
+
+class GasthausboxValidator(wrpylib.wrvalidators.GasthausboxDictValidator):
+    def __init__(self):
+        wrpylib.wrvalidators.GasthausboxDictValidator.__init__(self)
+        self.pre_validators=[wrpylib.mwmarkup.TemplateValidator(as_table=True, as_table_keylen=17), WinterrodelnTemplateDict('Gasthausbox')]
+        self.chained_validators = [GasthausboxDictConverter()]
 
 
 def gasthausbox_to_inn(wikitext, inn=None):
     """Converts a inn wiki page containing a {{Gasthausbox}} to an inn.
-    raises a formencode.Invalid exception if an error occurs.
+    inn may be an instance of WrInnCache or an "empty" class (default).
+    raises a formencode.Invalid exception if the format is not OK or the Gasthausbox is not found.
     :return: (start, end, inn) tuple."""
+    # find Gasthausbox
+    start, end = wrpylib.mwmarkup.find_template(wikitext, 'Gasthausbox')
+    if start is None: raise formencode.Invalid("No 'Gasthausbox' found", wikitext, None)
+
+    # convert to inn
     if inn is None:
-        class Inn(object): pass
-        inn = Inn()
-
-    # Match Gasthausbox
-    start, end = wrpylib.mwmarkup.find_template(wikitext, u'Gasthausbox')
-    if start is None: raise formencode.Invalid(u"No 'Gasthausbox' found", wikitext, None)
-    template_title, properties = wrpylib.mwmarkup.split_template(wikitext[start:end])
-
-    # Process properties
-    for key, value in properties.iteritems():
-        if   key == u'Position': inn.position_latitude, inn.position_longitude = _conv(wrpylib.wrvalidators.GeoNone().to_python, value, key) # '47.583333 N 15.75 E'
-        elif key == u'Höhe': inn.position_elevation = _conv(wrpylib.wrvalidators.UnsignedNone().to_python, value, key)
-        elif key == u'Betreiber': inn.operator = _conv(wrpylib.wrvalidators.UnicodeNone().to_python, value, key)
-        elif key == u'Sitzplätze': inn.seats = _conv(wrpylib.wrvalidators.UnsignedNone().to_python, value, key)
-        elif key == u'Übernachtung': inn.overnight, inn.overnight_comment = _conv(wrpylib.wrvalidators.BoolUnicodeTupleValidator().to_python, value, key)
-        elif key == u'Rauchfrei': inn.nonsmoker_area, inn.smoker_area = _conv(wrpylib.wrvalidators.GermanTristateTuple().to_python, value, key)
-        elif key == u'Rodelverleih': inn.sled_rental, inn.sled_rental_comment = _conv(wrpylib.wrvalidators.BoolUnicodeTupleValidator().to_python, value, key)
-        elif key == u'Handyempfang': inn.mobile_provider = _conv(wrpylib.wrvalidators.ValueCommentListNeinLoopNone().to_python, value, key)
-        elif key == u'Homepage': inn.homepage = _conv(wrpylib.wrvalidators.UrlNeinNone().to_python, value, key)
-        elif key == u'E-Mail': inn.email_list = _conv(wrpylib.wrvalidators.EmailCommentListNeinLoopNone(allow_masked_email=True).to_python, value, key)
-        elif key == u'Telefon': inn.phone_list = _conv(wrpylib.wrvalidators.PhoneCommentListNeinLoopNone(comments_are_optional=True).to_python, value, key)
-        elif key == u'Bild': inn.image = _conv(wrpylib.wrvalidators.UnicodeNone().to_python, value, key)
-        elif key == u'Rodelbahnen': inn.sledding_list = _conv(wrpylib.wrvalidators.WikiPageListLoopNone().to_python, value, key)
-        else: raise formencode.Invalid(u"Unbekannte Eigenschaft der Gasthausbox: '%s' (mit Wert '%s')" % (key, value), value, None)
-    return start, end, inn
+        state = None
+    else:
+        class State(object):
+            pass
+        state = State()
+        state.inn = inn
+    return start, end, GasthausboxValidator().to_python(wikitext[start:end], state)
 
 
 def inn_to_gasthausbox(inn):
     """Converts the inn class to the {{Gasthausbox}} representation."""
-    keys = []
-    values = []
-    keys.append(u'Position')
-    values.append(wrpylib.wrvalidators.GeoNone().from_python((inn.position_latitude, inn.position_longitude)))
-    keys.append(u'Höhe')
-    values.append(wrpylib.wrvalidators.UnsignedNone().from_python(inn.position_elevation))
-    keys.append(u'Betreiber')
-    values.append(wrpylib.wrvalidators.UnicodeNone().from_python(inn.operator))
-    keys.append(u'Sitzplätze')
-    values.append(wrpylib.wrvalidators.UnsignedNone().from_python(inn.seats))
-    keys.append(u'Übernachtung')
-    values.append(wrpylib.wrvalidators.BoolUnicodeTupleValidator().from_python((inn.overnight, inn.overnight_comment)))
-    keys.append(u'Rauchfrei')
-    values.append(wrpylib.wrvalidators.GermanTristateTuple().from_python((inn.nonsmoker_area, inn.smoker_area)))
-    keys.append(u'Rodelverleih')
-    values.append(wrpylib.wrvalidators.BoolUnicodeTupleValidator().from_python((inn.sled_rental, inn.sled_rental_comment)))
-    keys.append(u'Handyempfang')
-    values.append(wrpylib.wrvalidators.ValueCommentListNeinLoopNone().from_python(inn.mobile_provider))
-    keys.append(u'Homepage')
-    values.append(wrpylib.wrvalidators.UrlNeinNone().from_python(inn.homepage))
-    keys.append(u'E-Mail')
-    values.append(wrpylib.wrvalidators.EmailCommentListNeinLoopNone(allow_masked_email=True).from_python(inn.email_list))
-    keys.append(u'Telefon')
-    values.append(wrpylib.wrvalidators.PhoneCommentListNeinLoopNone(comments_are_optional=True).from_python(inn.phone_list))
-    keys.append(u'Bild')
-    values.append(wrpylib.wrvalidators.UnicodeNone().from_python(inn.image))
-    keys.append(u'Rodelbahnen')
-    values.append(wrpylib.wrvalidators.WikiPageListLoopNone().from_python(inn.sledding_list))
-    result = [u'{{Gasthausbox']
-    return wrpylib.mwmarkup.create_template(u'Gasthausbox', [], keys, values, True)
+    return GasthausboxValidator().from_python(inn)
 
 
 def find_template_latlon_ele(wikitext, template_title):
@@ -195,31 +230,31 @@ def find_template_latlon_ele(wikitext, template_title):
     start, end = wrpylib.mwmarkup.find_template(wikitext, template_title)
     if start is None: return (None,) * 5
     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
-    lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params[u'1'].strip())
-    ele = wrpylib.wrvalidators.UnsignedNone().to_python(params[u'2'].strip())
+    lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params['1'].strip())
+    ele = wrpylib.wrvalidators.UnsignedNone().to_python(params['2'].strip())
     return start, end, lat, lon, ele
 
 
 def create_template_latlon_ele(template_title, lat, lon, ele):
     geo = wrpylib.wrvalidators.GeoNone().from_python((lat, lon))
-    if len(geo) == 0: geo = u' '
+    if len(geo) == 0: geo = ' '
     ele = wrpylib.wrvalidators.UnsignedNone().from_python(ele)
-    if len(ele) == 0: ele = u' '
+    if len(ele) == 0: ele = ' '
     return wrpylib.mwmarkup.create_template(template_title, [geo, ele])
 
 
 def find_template_PositionOben(wikitext):
     """Same as find_template_latlon_ele with template '{{Position oben|47.076207 N 11.453553 E|1890}}'"""
-    return find_template_latlon_ele(wikitext, u'Position oben')
+    return find_template_latlon_ele(wikitext, 'Position oben')
 
 
 def create_template_PositionOben(lat, lon, ele):
-    return create_template_latlon_ele(u'Position, oben', lat, lon, ele)
+    return create_template_latlon_ele('Position, oben', lat, lon, ele)
 
 
 def find_template_PositionUnten(wikitext):
     """Same as find_template_latlon_ele with template '{{Position unten|47.076207 N 11.453553 E|1890}}'"""
-    return find_template_latlon_ele(wikitext, u'Position unten')
+    return find_template_latlon_ele(wikitext, 'Position unten')
 
 
 def find_template_unsigned(wikitext, template_title):
@@ -229,50 +264,50 @@ def find_template_unsigned(wikitext, template_title):
     start, end = wrpylib.mwmarkup.find_template(wikitext, template_title)
     if start is None: return (None,) * 3
     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
-    unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(params[u'1'].strip())
+    unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(params['1'].strip())
     return start, end, unsigned_value
 
 
 def create_template_unsigned(template_title, unsigned):
     unsigned = wrpylib.wrvalidators.UnsignedNone().from_python(unsigned)
-    if len(unsigned) == 0: unsigned = u' '
+    if len(unsigned) == 0: unsigned = ' '
     return wrpylib.mwmarkup.create_template(template_title, [unsigned])
 
 
 def find_template_Hoehenunterschied(wikitext):
     """Same as find_template_unsigned with template '{{Höhenunterschied|350}}'"""
-    return find_template_unsigned(wikitext, u'Höhenunterschied')
+    return find_template_unsigned(wikitext, 'Höhenunterschied')
 
 
 def create_template_Hoehenunterschied(ele_diff):
-    return create_template_unsigned(u'Höhenunterschied', ele_diff)
+    return create_template_unsigned('Höhenunterschied', ele_diff)
 
 
 def find_template_Bahnlaenge(wikitext):
     """Same as find_template_unsigned with template '{{Bahnlänge|4500}}'"""
-    return find_template_unsigned(wikitext, u'Bahnlänge')
+    return find_template_unsigned(wikitext, 'Bahnlänge')
 
 
 def create_template_Bahnlaenge(length):
-    return create_template_unsigned(u'Bahnlänge', length)
+    return create_template_unsigned('Bahnlänge', length)
 
 
 def find_template_Gehzeit(wikitext):
     """Same as find_template_unsigned with template '{{Gehzeit|60}}'"""
-    return find_template_unsigned(wikitext, u'Gehzeit')
+    return find_template_unsigned(wikitext, 'Gehzeit')
 
 
 def create_template_Gehzeit(walkup_time):
-    return create_template_unsigned(u'Gehzeit', walkup_time)
+    return create_template_unsigned('Gehzeit', walkup_time)
 
 
 def find_template_Forumlink(wikitext):
     """Same as find_template_unsigned with template '{{Forumlink|26}}'"""
-    start, end = wrpylib.mwmarkup.find_template(wikitext, u'Forumlink')
+    start, end = wrpylib.mwmarkup.find_template(wikitext, 'Forumlink')
     if start is None: return (None,) * 3
     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
-    forumid = params[u'1'].strip()
-    if forumid == u'<nummer einfügen>': unsigned_value = None
+    forumid = params['1'].strip()
+    if forumid == '<nummer einfügen>': unsigned_value = None
     else: unsigned_value = wrpylib.wrvalidators.UnsignedNone().to_python(forumid)
     return start, end, unsigned_value
     # return find_template_unsigned(wikitext, u'Forumlink')
@@ -280,27 +315,27 @@ def find_template_Forumlink(wikitext):
 
 def find_template_Parkplatz(wikitext):
     """Same as find_template_latlon_ele with template '{{Parkplatz|47.076207 N 11.453553 E|1890}}'"""
-    return find_template_latlon_ele(wikitext, u'Parkplatz')
+    return find_template_latlon_ele(wikitext, 'Parkplatz')
 
 
 def find_template_Haltestelle(wikitext):
     """Finds the first occurance of the '{{Haltestelle|Ortsname|Haltestellenname|47.076207 N 11.453553 E|1890}}' template
     and returns the tuple (start, end, city, stop, lat, lon, ele) or (None, None, None, None, None, None, None) if the
     template was not found. If the template has no valid format, an exception is thrown."""
-    start, end = wrpylib.mwmarkup.find_template(wikitext, u'Haltestelle')
+    start, end = wrpylib.mwmarkup.find_template(wikitext, 'Haltestelle')
     if start is None: return (None,) * 7
     title, params = wrpylib.mwmarkup.split_template(wikitext[start:end])
-    city = wrpylib.wrvalidators.UnicodeNone().to_python(params[u'1'].strip())
-    stop = wrpylib.wrvalidators.UnicodeNone().to_python(params[u'2'].strip())
-    lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params[u'3'].strip())
-    ele = wrpylib.wrvalidators.UnsignedNone().to_python(params[u'4'].strip())
+    city = wrpylib.wrvalidators.UnicodeNone().to_python(params['1'].strip())
+    stop = wrpylib.wrvalidators.UnicodeNone().to_python(params['2'].strip())
+    lat, lon = wrpylib.wrvalidators.GeoNone().to_python(params['3'].strip())
+    ele = wrpylib.wrvalidators.UnsignedNone().to_python(params['4'].strip())
     return start, end, city, stop, lat, lon, ele
 
 
 def find_all_templates(wikitext, find_func):
     """Returns a list of return values of find_func that searches for a template.
     Example:
-    >>> find_all_tempaltes(wikitext, find_template_Haltestelle)
+    >>> find_all_templates(wikitext, find_template_Haltestelle)
     Returns an empty list if the template was not found at all.
     """
     results = []
@@ -318,55 +353,45 @@ def find_all_templates(wikitext, find_func):
     return results
 
 
-def parse_googlemap(wikitext):
-    """Parses the (unicode) u'<googlemap ...>content</googlemap>' of the googlemap extension
-    out of a page. If wikitext does not contain the googlemaps extension text None is returned.
-    If the googlemap contains invalid formatted lines, a RuntimeError is raised.
-
-    :param wikitext: wikitext containing the template. Example:
-
-    wikitext = '''
-    <googlemap version="0.9" lat="47.113291" lon="11.272337" zoom="15">
-    (Parkplatz)47.114958,11.266026
-    Parkplatz
-    
-    (Gasthaus) 47.114715, 11.266262, Alt Bärnbad (Gasthaus)
-    6#FF014E9A
-    47.114715,11.266262
-    47.114135,11.268381
-    47.113421,11.269322
-    47.11277,11.269979
-    47.112408,11.271119
-    </googlemap>
-    '''
-    :returns: (attributes, GeoJSON as Python datatypes)
+def googlemap_to_wrmap(attributes, coords, paths):
+    """Converts the output of parse_googlemap to the GeoJSON format wrmap uses.
+    :returns: (GeoJSON as nested Python datatypes)
     """
-    center, zoom, coords, paths = wrpylib.mwmarkup.parse_googlemap(wikitext)
     json_features = []
 
     # point
     for point in coords:
         lon, lat, symbol, title = point
-        properties = {'type': symbol}
-        if title: properties['name'] = title
+        properties = {'type': 'punkt' if symbol is None else symbol.lower()}
+        if title is not None: properties['name'] = title
         json_features.append({
-            'type': 'feature',
+            'type': 'Feature',
             'geometry': {'type': 'Point', 'coordinates': [lon, lat]},
             'properties': properties})
         
     # path
     for path in paths:
         style, entries = path
-        properties = {'type': 'line'}
+        style = style.lower()
+        PATH_TYPES = {'6#ff014e9a': 'rodelbahn', '6#ffe98401': 'gehweg', '6#ff7f7fff': 'alternative', '3#ff000000': 'lift', '3#ffe1e100': 'anfahrt'}
+        if style in PATH_TYPES:
+            properties = {'type': PATH_TYPES[style]}
+        else:
+            properties = {'type': 'line'}
+            properties['dicke'] = style[0]
+            properties['farbe'] = style[4:]
         json_features.append({
-            'type': 'feature',
+            'type': 'Feature',
             'geometry': {
                 'type': 'LineString',
                 'coordinates': [[lon, lat] for lon, lat, symbol, title in entries]},
             'properties': properties})
 
-    geojson = {'type': 'FeatureCollection', 'features': json_features}
-    return {'lon': center[0], 'lat': center[1], 'zoom': zoom}, geojson
+    geojson = {
+            'type': 'FeatureCollection',
+            'features': json_features,
+            'properties': attributes}
+    return geojson
 
 
 def parse_wrmap_coordinates(coords):
@@ -389,11 +414,13 @@ def parse_wrmap_coordinates(coords):
 
 
 def parse_wrmap(wikitext):
-    """Parses the (unicode) u'<wrmap ...>content</wrmap>' of the Winterrodeln wrmap extension
-    out of a page. If wikitext does not contain the googlemaps extension text None is returned.
-    If the googlemap contains invalid formatted lines, a RuntimeError is raised.
+    """Parses the (unicode) u'<wrmap ...>content</wrmap>' of the Winterrodeln wrmap extension.
+    If wikitext does not contain the <wrmap> tag or if the <wrmap> tag contains 
+    invalid formatted lines, a ParseError is raised.
+    Use wrpylib.mwmarkup.find_tag(wikitext, 'wrmap') to find the wrmap tag within an arbitrary
+    wikitext before using this function.
 
-    :param wikitext: wikitext containing the template. Example:
+    :param wikitext: wikitext containing only the template. Example:
 
     wikitext = u'''
     <wrmap lat="47.2417134" lon="11.21408895" zoom="14" width="700" height="400">
@@ -407,91 +434,85 @@ def parse_wrmap(wikitext):
     </rodelbahn>
     </wrmap>
     '''
-    :returns: (attributes, GeoJSON as Python datatypes)
+    :returns: GeoJSON as nested Python datatype
     """
     # parse XML
     try:
         wrmap_xml = xml.etree.ElementTree.fromstring(wikitext.encode('utf-8'))
     except xml.etree.ElementTree.ParseError as e:
         row, column = e.position
-        raise RuntimeError("XML parse error on row {}, column {}: {}".format(row, column, e))
-    # assert(in_array($tagname, array('wrmap', 'wrgmap'))); # TODO
+        raise ParseError("XML parse error on row {}, column {}: {}".format(row, column, e))
+    if wrmap_xml.tag not in ['wrmap', 'wrgmap']:
+        raise ParseError('No valid tag name')
 
     # convert XML to geojson (http://www.geojson.org/geojson-spec.html)
     json_features = []
     for feature in wrmap_xml:
         # determine feature type
-        is_point = WRMAP_POINT_TYPE.has_key(feature.tag)
-        is_line = WRMAP_LINE_TYPE.has_key(feature.tag)
+        is_point = feature.tag in WRMAP_POINT_TYPES
+        is_line = feature.tag in WRMAP_LINE_TYPES
         if (not is_point and not is_line):
-            raise RuntimeError('Unknown element <{}>.'.format(feature.tag))
+            raise ParseError('Unknown element <{}>.'.format(feature.tag))
 
         # point
         if is_point:
-            properties = {'type': WRMAP_POINT_TYPE[feature.tag]}
+            properties = {'type': feature.tag}
             allowed_properties = set(['name', 'wiki'])
             wrong_properties = set(feature.attrib.keys()) - allowed_properties
             if len(wrong_properties) > 0:
-                raise RuntimeError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
+                raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
             properties.update(feature.attrib)
             coordinates = parse_wrmap_coordinates(feature.text)
             if len(coordinates) != 1:
-                raise RuntimeError('The element <{}> has to have exactly one coordinate pair.'.format(feature.tag))
+                raise ParseError('The element <{}> has to have exactly one coordinate pair.'.format(feature.tag))
             json_features.append({
-                'type': 'feature',
+                'type': 'Feature',
                 'geometry': {'type': 'Point', 'coordinates': coordinates[0]},
                 'properties': properties})
 
         # line
         if is_line:
-            properties = {'type': WRMAP_LINE_TYPE[feature.tag]}
+            properties = {'type': feature.tag}
             allowed_properties = set(['farbe', 'dicke'])
             wrong_properties = set(feature.attrib.keys()) - allowed_properties
             if len(wrong_properties) > 0:
-                raise RuntimeError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
-            if feature.attrib.has_key('farbe')
+                raise ParseError("The attribute '{}' is not allowed at <{}>.".format(list(wrong_properties)[0], feature.tag))
+            if 'farbe' in feature.attrib
                 if not re.match('#[0-9a-fA-F]{6}$', feature.attrib['farbe']):
-                    raise RuntimeError('The attribute "farbe" has to have a format like "#a0bb43".')
+                    raise ParseError('The attribute "farbe" has to have a format like "#a0bb43".')
                 properties['strokeColor'] = feature.attrib['farbe'] # e.g. #a200b7
-            if feature.attrib.has_key('dicke'):
+            if 'dicke' in feature.attrib:
                 try:
                     properties['strokeWidth'] = int(feature.attrib['dicke']) # e.g. 6
                 except ValueError:
-                    raise RuntimeError('The attribute "dicke" has to be an integer.')
+                    raise ParseError('The attribute "dicke" has to be an integer.')
             json_features.append({
-                'type': 'feature',
+                'type': 'Feature',
                 'geometry': {'type': 'LineString', 'coordinates': parse_wrmap_coordinates(feature.text)},
                 'properties': properties})
 
+    # attributes
+    properties = {}
+    for k, v in wrmap_xml.attrib.items():
+        if k in ['lat', 'lon']:
+            try:
+                properties[k] = float(v)
+            except ValueError:
+                raise ParseError('Attribute "{}" has to be a float value.'.format(k))
+        elif k in ['zoom', 'width', 'height']:
+            try:
+                properties[k] = int(v)
+            except ValueError:
+                raise ParseError('Attribute "{}" has to be an integer value.'.format(k))
+        else:
+            raise ParseError('Unknown attribute "{}".'.format(k))
+
     geojson = {
         'type': 'FeatureCollection',
-        'features': json_features}
+        'features': json_features,
+        'properties': properties}
 
-    # attributes
-    attributes = {}
-    try:
-        attributes['lat'] = float(wrmap_xml.attrib.get('lat', 47.267648)) # center lat
-    except ValueError:
-        raise RuntimeError('Attribute "lat" has to be a float value.')
-    try:
-        attributes['lon'] = float(wrmap_xml.attrib.get('lon', 11.404655)) # center lon
-    except ValueError:
-        raise RuntimeError('Attribute "lon" has to be a float value.')
-    try:
-        attributes['zoom'] = int(wrmap_xml.attrib.get('zoom', 10)) # Google Zoom Level
-    except ValueError:
-        raise RuntimeError('Attribute "zoom" has to be an integer value.')
-    try:
-        attributes['width'] = int(wrmap_xml.attrib['width']) if wrmap_xml.attrib.has_key('width') else None # None corresponds to 100%
-    except ValueError:
-        raise RuntimeError('Attribute "width" has to be an integer value.')
-    try:
-        attributes['height'] = int(wrmap_xml.attrib.get('height', 450)) # map height in px
-    except ValueError:
-        raise RuntimeError('Attribute "height" has to be an integer value.')
-    # show_sledruns = (wrmap_xml.tag == 'wrgmap')
-
-    return attributes, geojson
+    return geojson
 
 
 def create_wrmap_coordinates(coords):
@@ -501,26 +522,35 @@ def create_wrmap_coordinates(coords):
     return '\n'.join(result)
  
 
-def create_wrmap(attributes, geojson):
+def create_wrmap(geojson):
     """Creates a <wrmap> wikitext from geojson (as python types)."""
     wrmap_xml = xml.etree.ElementTree.Element('wrmap')
-    for k, v in attributes.iteritems():
-        wrmap_xml.attrib[k] = str(v)
+    wrmap_xml.text = '\n\n'
+    for k, v in geojson['properties'].items():
+        if k in ['lon', 'lat']:
+            wrmap_xml.attrib[k] = '{:.6f}'.format(v)
+        else:
+            wrmap_xml.attrib[k] = str(v)
 
     assert geojson['type'] == 'FeatureCollection'
     json_features = geojson['features']
+    last_json_feature = None
     for json_feature in json_features:
+        feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, json_feature['properties']['type'])
         geo = json_feature['geometry']
         if geo['type'] == 'Point':
-            wrmap_type = dict(zip(WRMAP_POINT_TYPE.values(), WRMAP_POINT_TYPE.keys()))
-            feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, wrmap_type[json_feature['properties']['type']])
             feature_xml.text = create_wrmap_coordinates([geo['coordinates']])
-            feature_xml.attrib = json_feature['properties']
-        if geo['type'] == 'LineString':
-            wrmap_type = dict(zip(WRMAP_LINE_TYPE.values(), WRMAP_LINE_TYPE.keys()))
-            feature_xml = xml.etree.ElementTree.SubElement(wrmap_xml, wrmap_type[json_feature['properties']['type']])
-            feature_xml.text = create_wrmap_coordinates(geo['coordinates'])
-            feature_xml.attrib = json_feature['properties']
-
-    return xml.etree.ElementTree.tostring(wrmap_xml)
+            if last_json_feature is not None:
+                last_json_feature.tail = '\n'
+        else:
+            if last_json_feature is not None:
+                last_json_feature.tail = '\n\n'
+            feature_xml.text = '\n' + create_wrmap_coordinates(geo['coordinates']) + '\n'
+        last_json_feature = feature_xml
+        feature_xml.attrib = json_feature['properties']
+        del feature_xml.attrib['type']
+
+    if last_json_feature is not None:
+        last_json_feature.tail = '\n\n'
+    return xml.etree.ElementTree.tostring(wrmap_xml, encoding='utf-8').decode('utf-8')