Intermediate commit
authorphilipp <philipp@7aebc617-e5e2-0310-91dc-80fb5f6d2477>
Tue, 2 Feb 2016 21:08:54 +0000 (21:08 +0000)
committerphilipp <philipp@7aebc617-e5e2-0310-91dc-80fb5f6d2477>
Tue, 2 Feb 2016 21:08:54 +0000 (21:08 +0000)
git-svn-id: http://www.winterrodeln.org/svn/wrpylib/trunk@2428 7aebc617-e5e2-0310-91dc-80fb5f6d2477

tests/test_mwmarkup.py
tests/test_wrmwmarkup.py
tests/test_wrvalidators.py
wrpylib/mwmarkup.py
wrpylib/wrmwmarkup.py
wrpylib/wrvalidators.py

index a744bfa150217c17f5cbc3e7f130ab5a41c4e55c..7b0fb213604b742adc3b4e22088ff09083e57c23 100644 (file)
@@ -1,7 +1,8 @@
 #!/usr/bin/python3.4
-# -*- coding: iso-8859-15 -*-
-import wrpylib.mwmarkup
+# coding=utf-8
 import unittest
+import mwparserfromhell
+import wrpylib.mwmarkup
 
 
 class TestMwMarkup(unittest.TestCase):
@@ -10,57 +11,57 @@ class TestMwMarkup(unittest.TestCase):
         {{Rodelbahnbox
         | Position             = 47.309820 N 9.986508 E
         | Position oben        =
-        | Höhe oben            = 1244
+        | Höhe oben            = 1244
         | Position unten       =
-        | Höhe unten           = 806
-        | Länge                = 5045
+        | Höhe unten           = 806
+        | Länge                = 5045
         | Schwierigkeit        =
         | Lawinen              = gelegentlich
         | Betreiber            =
-        | Öffentliche Anreise  = Ja
+        | Öffentliche Anreise  = Ja
         | Gehzeit              = 105
         | Aufstieg getrennt    = Nein
         | Aufstiegshilfe       = Nein
         | Beleuchtungsanlage   = Nein
         | Beleuchtungstage     =
         | Rodelverleih         = Ja
-        | Gütesiegel           =
+        | Gütesiegel           =
         | Webauskunft          =
-        | Telefonauskunft      = +43-664-1808482 (Bergkristallhütte)
-        | Bild                 = Rodelbahn Bergkristallhütte 2009-03-03.jpg
-        | In Übersichtskarte   = Ja
+        | Telefonauskunft      = +43-664-1808482 (Bergkristallhütte)
+        | Bild                 = Rodelbahn Bergkristallhütte 2009-03-03.jpg
+        | In Übersichtskarte   = Ja
         | Forumid              = 72
         }}
-        Die Rodelbahn zur Bergkristallhütte ist durchaus abwechslungsreich.'''
+        Die Rodelbahn zur Bergkristallhütte ist durchaus abwechslungsreich.'''
         start, end = wrpylib.mwmarkup.find_template(wikitext, 'Rodelbahnbox')
-        assert start == wikitext.find('{{')
-        assert end == wikitext.find('}}')+2
+        self.assertEqual(start, wikitext.find('{{'))
+        self.assertEqual(end, wikitext.find('}}')+2)
 
 
     def test_TemplateValidator(self):
         v = wrpylib.mwmarkup.TemplateValidator()
-        value = '{{Rodelbahnbox | Unbenannt | Position = 47.309820 N 9.986508 E | Aufstieg möglich = Ja }}'
+        value = '{{Rodelbahnbox | Unbenannt | Position = 47.309820 N 9.986508 E | Aufstieg möglich = Ja }}'
         title, anonym_params, named_params = v.to_python(value)
-        assert title == 'Rodelbahnbox'
-        assert anonym_params == ['Unbenannt']
-        assert list(named_params.keys()) == ['Position', 'Aufstieg möglich']
-        assert list(named_params.values()) == ['47.309820 N 9.986508 E', 'Ja']
+        self.assertEqual(title, 'Rodelbahnbox')
+        self.assertEqual(anonym_params, ['Unbenannt'])
+        self.assertEqual(list(named_params.keys()), ['Position', 'Aufstieg möglich'])
+        self.assertEqual(list(named_params.values()), ['47.309820 N 9.986508 E', 'Ja'])
         value2 = v.from_python((title, anonym_params, named_params))
-        assert value2 == '{{Rodelbahnbox|Unbenannt|Position=47.309820 N 9.986508 E|Aufstieg möglich=Ja}}'
+        self.assertEqual(value2, '{{Rodelbahnbox|Unbenannt|Position=47.309820 N 9.986508 E|Aufstieg möglich=Ja}}')
         v = wrpylib.mwmarkup.TemplateValidator(as_table=True)
         value3 = v.from_python((title, anonym_params, named_params))
-        assert value3 == \
-        '{{Rodelbahnbox\n' + \
-        '| Unbenannt\n' + \
-        '| Position         = 47.309820 N 9.986508 E\n' + \
-        '| Aufstieg möglich = Ja\n' + \
-        '}}'
+        self.assertEqual(value3,
+            '{{Rodelbahnbox\n' +
+            '| Unbenannt\n' +
+            '| Position         = 47.309820 N 9.986508 E\n' +
+            '| Aufstieg möglich = Ja\n' +
+            '}}')
         v = wrpylib.mwmarkup.TemplateValidator(strip=False)
         title, anonym_params, named_params = v.to_python(value)
-        assert title == 'Rodelbahnbox '
-        assert anonym_params == [' Unbenannt ']
-        assert list(named_params.keys()) == [' Position ', ' Aufstieg möglich ']
-        assert list(named_params.values()) == [' 47.309820 N 9.986508 E ', ' Ja ']
+        self.assertEqual(title, 'Rodelbahnbox ')
+        self.assertEqual(anonym_params, [' Unbenannt '])
+        self.assertEqual(list(named_params.keys()), [' Position ', ' Aufstieg möglich '])
+        self.assertEqual(list(named_params.values()), [' 47.309820 N 9.986508 E ', ' Ja '])
 
 
     def test_split_template(self):
@@ -68,34 +69,34 @@ class TestMwMarkup(unittest.TestCase):
         {{Rodelbahnbox
         | Position             = 47.309820 N 9.986508 E
         | Position oben        =
-        | Höhe oben            = 1244
+        | Höhe oben            = 1244
         | Position unten       =
-        | Höhe unten           = 806
-        | Länge                = 5045
+        | Höhe unten           = 806
+        | Länge                = 5045
         | Schwierigkeit        =
         | Lawinen              = gelegentlich
         | Betreiber            =
-        | Öffentliche Anreise  = Ja
+        | Öffentliche Anreise  = Ja
         | Gehzeit              = 105
         | Aufstieg getrennt    = Nein
         | Aufstiegshilfe       = Nein
         | Beleuchtungsanlage   = Nein
         | Beleuchtungstage     =
         | Rodelverleih         = Ja
-        | Gütesiegel           =
+        | Gütesiegel           =
         | Webauskunft          =
-        | Telefonauskunft      = +43-664-1808482 (Bergkristallhütte)
-        | Bild                 = Rodelbahn Bergkristallhütte 2009-03-03.jpg
-        | In Übersichtskarte   = Ja
+        | Telefonauskunft      = +43-664-1808482 (Bergkristallhütte)
+        | Bild                 = Rodelbahn Bergkristallhütte 2009-03-03.jpg
+        | In Übersichtskarte   = Ja
         | Forumid              = 72
         }}
-        Die Rodelbahn zur Bergkristallhütte ist durchaus abwechslungsreich.'''
+        Die Rodelbahn zur Bergkristallhütte ist durchaus abwechslungsreich.'''
         start, end = wrpylib.mwmarkup.find_template(wikitext, 'Rodelbahnbox')
         template_title, parameters = wrpylib.mwmarkup.split_template(wikitext[start:end])
         assert template_title == 'Rodelbahnbox'
         assert len(parameters) == 22
         assert parameters['Position'] == '47.309820 N 9.986508 E'
-        assert parameters['Telefonauskunft'] == '+43-664-1808482 (Bergkristallhütte)'
+        assert parameters['Telefonauskunft'] == '+43-664-1808482 (Bergkristallhütte)'
         assert parameters['Schwierigkeit'] == ''
 
 
@@ -128,7 +129,7 @@ class TestMwMarkup(unittest.TestCase):
         (Parkplatz)47.114958,11.266026
         Erster Parkplatz
 
-        (Gasthaus) 47.114715, 11.266262, Alt Bärnbad (Gasthaus)
+        (Gasthaus) 47.114715, 11.266262, Alt Bärnbad (Gasthaus)
         6#FF014E9A
         47.114715,11.266262
         47.114135,11.268381
@@ -143,7 +144,7 @@ class TestMwMarkup(unittest.TestCase):
         assert attributes['zoom'] == 15
         assert coords == [
             (11.266026, 47.114958, 'Parkplatz', 'Erster Parkplatz'),
-            (11.266262, 47.114715, 'Gasthaus', 'Alt Bärnbad (Gasthaus)')]
+            (11.266262, 47.114715, 'Gasthaus', 'Alt Bärnbad (Gasthaus)')]
         assert paths == [
             ('6#FF014E9A', [
                 (11.266262, 47.114715, None, None),
@@ -157,3 +158,169 @@ class TestMwMarkup(unittest.TestCase):
         except wrpylib.mwmarkup.ParseError:
             pass
 
+
+class TestMwParserFromHell(unittest.TestCase):
+    def test_find_template(self):
+        wikitext = '''== Allgemeines ==
+        {{Rodelbahnbox
+        | Position             = 47.309820 N 9.986508 E
+        | Position oben        =
+        | Höhe oben            = 1244
+        | Position unten       =
+        | Höhe unten           = 806
+        | Länge                = 5045
+        | Schwierigkeit        =
+        | Lawinen              = gelegentlich
+        | Betreiber            =
+        | Öffentliche Anreise  = Ja
+        | Gehzeit              = 105
+        | Aufstieg getrennt    = Nein
+        | Aufstiegshilfe       = Nein
+        | Beleuchtungsanlage   = Nein
+        | Beleuchtungstage     =
+        | Rodelverleih         = Ja
+        | Gütesiegel           =
+        | Webauskunft          =
+        | Telefonauskunft      = +43-664-1808482 (Bergkristallhütte)
+        | Bild                 = Rodelbahn Bergkristallhütte 2009-03-03.jpg
+        | In Übersichtskarte   = Ja
+        | Forumid              = 72
+        }}
+        Die Rodelbahn zur Bergkristallhütte ist durchaus abwechslungsreich.'''
+        wikicode = mwparserfromhell.parse(wikitext)
+        rb = list(wikicode.filter_templates())[0]
+        self.assertEqual(rb.name.strip(), 'Rodelbahnbox')
+        self.assertEqual(rb.get('Aufstiegshilfe').value.strip(), 'Nein')
+        self.assertEqual(rb[:2], '{{')
+        self.assertEqual(rb[-2:], '}}')
+
+    def test_template_to_table(self):
+        wikitext = '{{Rodelbahnbox | Unbenannt | Position = 47.309820 N 9.986508 E | Aufstieg möglich = Ja }}'
+        wikicode = mwparserfromhell.parse(wikitext)
+        template = list(wikicode.filter_templates())[0]
+        self.assertEqual(template.name.strip(), 'Rodelbahnbox')
+        self.assertEqual(template.params[0].strip(), 'Unbenannt')
+        self.assertEqual(template.params[1].name.strip(), 'Position')
+        self.assertEqual(template.params[1].value.strip(), '47.309820 N 9.986508 E')
+        self.assertEqual(template.params[2].name.strip(), 'Aufstieg möglich')
+        self.assertEqual(template.params[2].value.strip(), 'Ja')
+
+        template = mwparserfromhell.nodes.template.Template('Rodelbahnbox')
+        template.add(1, 'Unbenannt')
+        template.add('Position', '47.309820 N 9.986508 E')
+        template.add('Aufstieg möglich', 'Ja')
+        self.assertEqual(template, '{{Rodelbahnbox|Unbenannt|Position=47.309820 N 9.986508 E|Aufstieg möglich=Ja}}')
+
+        wrpylib.mwmarkup.template_to_table(template)
+        self.assertEqual(template,
+            '{{Rodelbahnbox\n' +
+            '| Unbenannt\n' +
+            '| Position         = 47.309820 N 9.986508 E\n' +
+            '| Aufstieg möglich = Ja\n' +
+            '}}')
+
+        wrpylib.mwmarkup.template_to_table(template, 18)
+        self.assertEqual(template,
+            '{{Rodelbahnbox\n' +
+            '| Unbenannt\n' +
+            '| Position          = 47.309820 N 9.986508 E\n' +
+            '| Aufstieg möglich  = Ja\n' +
+            '}}')
+
+    def test_split_template(self):
+        wikitext = '''== Allgemeines ==
+        {{Rodelbahnbox
+        | Position             = 47.309820 N 9.986508 E
+        | Position oben        =
+        | Höhe oben            = 1244
+        | Position unten       =
+        | Höhe unten           = 806
+        | Länge                = 5045
+        | Schwierigkeit        =
+        | Lawinen              = gelegentlich
+        | Betreiber            =
+        | Öffentliche Anreise  = Ja
+        | Gehzeit              = 105
+        | Aufstieg getrennt    = Nein
+        | Aufstiegshilfe       = Nein
+        | Beleuchtungsanlage   = Nein
+        | Beleuchtungstage     =
+        | Rodelverleih         = Ja
+        | Gütesiegel           =
+        | Webauskunft          =
+        | Telefonauskunft      = +43-664-1808482 (Bergkristallhütte)
+        | Bild                 = Rodelbahn Bergkristallhütte 2009-03-03.jpg
+        | In Übersichtskarte   = Ja
+        | Forumid              = 72
+        }}
+        Die Rodelbahn zur Bergkristallhütte ist durchaus abwechslungsreich.'''
+        wikicode = mwparserfromhell.parse(wikitext)
+        template = wikicode.filter_templates(matches='Rodelbahnbox')[0]
+        self.assertEqual(template.name.strip(), 'Rodelbahnbox')
+        self.assertEqual(len(template.params), 22)
+        self.assertEqual(template.get('Position').value.strip(), '47.309820 N 9.986508 E')
+        self.assertEqual(template.get('Telefonauskunft').value.strip(), '+43-664-1808482 (Bergkristallhütte)')
+        self.assertEqual(template.get('Schwierigkeit').value.strip(), '')
+
+    def test_create_template(self):
+        template = mwparserfromhell.nodes.template.Template('Rodelbahnbox')
+        template.add(1, 'Unbenannt')
+        template.add('Position', '47.309820 N 9.986508 E')
+        template.add('Aufstieg möglich', 'Ja')
+        self.assertEqual(template, '{{Rodelbahnbox|Unbenannt|Position=47.309820 N 9.986508 E|Aufstieg möglich=Ja}}')
+
+        wrpylib.mwmarkup.template_to_table(template)
+        self.assertEqual(template,
+            '{{Rodelbahnbox\n' +
+            '| Unbenannt\n' +
+            '| Position         = 47.309820 N 9.986508 E\n' +
+            '| Aufstieg möglich = Ja\n' +
+            '}}')
+
+    def test_find_tag(self):
+        wikitext = 'This is <tag>my first tag</tag> and <tag>my second tag</tag>.'
+        wikicode = mwparserfromhell.parse(wikitext)
+        tag_iter = wikicode.ifilter_tags()
+        tag = next(tag_iter)
+        self.assertEqual(tag.tag.strip(), 'tag')
+        self.assertEqual(tag.contents.strip(), 'my first tag')
+        tag = next(tag_iter)
+        self.assertEqual(tag.tag.strip(), 'tag')
+        self.assertEqual(tag.contents.strip(), 'my second tag')
+
+        wikitext = 'This is <tag myattrib="4"/>.'
+        wikicode = mwparserfromhell.parse(wikitext)
+        tag = next(wikicode.ifilter_tags())
+        self.assertEqual('tag', tag.tag)
+
+    def test_parse_googlemap(self):
+        wikitext = '''
+        <googlemap version="0.9" lat="47.113291" lon="11.272337" zoom="15">
+        (Parkplatz)47.114958,11.266026
+        Erster 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>
+        '''
+        attributes, coords, paths = wrpylib.mwmarkup.parse_googlemap(wikitext)
+        self.assertEqual(attributes['lon'], 11.272337)
+        self.assertEqual(attributes['lat'], 47.113291)
+        self.assertEqual(attributes['zoom'], 15)
+        self.assertEqual(coords, [
+            (11.266026, 47.114958, 'Parkplatz', 'Erster Parkplatz'),
+            (11.266262, 47.114715, 'Gasthaus', 'Alt Bärnbad (Gasthaus)')])
+        self.assertEqual(paths, [
+            ('6#FF014E9A', [
+                (11.266262, 47.114715, None, None),
+                (11.268381, 47.114135, None, None),
+                (11.269322, 47.113421, None, None),
+                (11.269979, 47.11277, None, None),
+                (11.271119, 47.112408, None, None)])])
+        with self.assertRaises(wrpylib.mwmarkup.ParseError):
+            wrpylib.mwmarkup.parse_googlemap(wikitext.replace('<googlemap', '|googlemap'))
index bbbca5c70950dd0dab1702a4c67ee5deda6b8dae..c9e60f0201df42b650e324a09223aacc98a536c4 100644 (file)
@@ -1,10 +1,10 @@
 #!/usr/bin/python3.4
 # -*- coding: iso-8859-15 -*-
 import collections
-import wrpylib.wrmwmarkup
-import wrpylib.mwmarkup
 import textwrap
 import unittest
+import wrpylib.mwmarkup
+import wrpylib.wrmwmarkup
 
 
 class TestWrMwMarkup(unittest.TestCase):
@@ -235,7 +235,6 @@ class TestWrMwMarkup(unittest.TestCase):
             [11.269979, 47.11277],
             [11.271119, 47.112408]]
 
-
     def test_parse_wrmap(self):
         wikitext = '''
         <wrmap lat="47.2417134" lon="11.21408895" zoom="14" width="700" height="400">
@@ -270,7 +269,6 @@ class TestWrMwMarkup(unittest.TestCase):
             [11.230868, 47.244951],
             [11.237853, 47.245470]]
 
-
     def test_create_wrmap(self):
         geojson = {
             'type': 'FeatureCollection',
index a397e3dfa405b929db5a1d797f868aef7f29bf01..8cc33e636f46d92fd2c352cd460fbb5da34b1721 100644 (file)
@@ -4,189 +4,496 @@ import collections
 import wrpylib.wrvalidators
 import formencode
 import unittest
+from wrpylib.wrvalidators import *
+
+
+class TestConverter(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(Converter.from_str('abc'), 'abc')
+        self.assertEqual(Converter.from_str(''), '')
+
+    def test_to_str(self):
+        self.assertEqual(Converter.to_str('abc'), 'abc')
+        self.assertEqual(Converter.to_str(''), '')
+        self.assertEqual(Converter.to_str(42), '42')
+
+
+class TestInt(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(int_from_str('42'), 42)
+        self.assertEqual(int_from_str('+42'), 42)
+        self.assertEqual(int_from_str('-20'), -20)
+        self.assertEqual(int_from_str('0', min=0), 0)
+        self.assertEqual(int_from_str('10', max=10), 10)
+        with self.assertRaises(ValueError):
+            int_from_str('abc')
+        with self.assertRaises(ValueError):
+            int_from_str('')
+        with self.assertRaises(ValueError):
+            int_from_str('-1', min=0)
+        with self.assertRaises(ValueError):
+            int_from_str('11', max=10)
+        with self.assertRaises(ValueError):
+            int_from_str('10.0')
+        with self.assertRaises(ValueError):
+            int_from_str('0d')
+
+    def test_to_str(self):
+        self.assertEqual(int_to_str(20), '20')
+        self.assertEqual(int_to_str(-20), '-20')
+        self.assertEqual(int_to_str(0), '0')
+
+
+class TestOptInt(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(opt_int_from_str('42'), 42)
+        self.assertEqual(opt_int_from_str('+42'), 42)
+        self.assertEqual(opt_int_from_str('-20'), -20)
+        self.assertEqual(opt_int_from_str(''), None)
+        with self.assertRaises(ValueError):
+            opt_int_from_str('abc')
+        with self.assertRaises(ValueError):
+            opt_int_from_str('10.0')
+        with self.assertRaises(ValueError):
+            opt_int_from_str('0d')
+
+    def test_to_str(self):
+        self.assertEqual(opt_int_to_str(20), '20')
+        self.assertEqual(opt_int_to_str(-20), '-20')
+        self.assertEqual(opt_int_to_str(0), '0')
+        self.assertEqual(opt_int_to_str(None), '')
+
+
+class TestEnumConverter(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(enum_from_str(''), [])
+        self.assertEqual(enum_from_str('abc'), ['abc'])
+        self.assertEqual(enum_from_str('abc; def'), ['abc', 'def'])
+        self.assertEqual(enum_from_str('abc; def;ghi'), ['abc', 'def', 'ghi'])
+
+    def test_to_str(self):
+        self.assertEqual(enum_to_str(['abc', 'def', 'ghi']), 'abc; def; ghi')
+        self.assertEqual(enum_to_str(['abc']), 'abc')
+        self.assertEqual(enum_to_str(['']), '')
+        self.assertEqual(enum_to_str([]), '')
+
+
+class TestDateTime(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(DateTime.from_str('2015-12-31 23:07:42'), datetime.datetime(2015, 12, 31, 23, 7, 42))
+
+    def test_to_str(self):
+        self.assertEqual(DateTime.to_str(datetime.datetime(2015, 12, 31, 23, 7, 42)), '2015-12-31 23:07:42')
+
+
+class TestDifficultyGerman(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(difficulty_german_from_str('leicht'), 1)
+        self.assertEqual(difficulty_german_from_str('mittel'), 2)
+        with self.assertRaises(ValueError):
+            difficulty_german_from_str('dontknow')
+        with self.assertRaises(ValueError):
+            difficulty_german_from_str('')
+
+    def test_to_str(self):
+        self.assertEqual(difficulty_german_to_str(1), 'leicht')
+
+
+class TestTristateGerman(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(tristate_german_from_str('Ja'), 1.0)
+        self.assertEqual(tristate_german_from_str('Teilweise'), 0.5)
+        self.assertEqual(tristate_german_from_str('Nein'), 0)
+        with self.assertRaises(ValueError):
+            tristate_german_from_str('')
+        with self.assertRaises(ValueError):
+            tristate_german_from_str('Vielleicht')
+
+    def test_to_str(self):
+        self.assertEqual(tristate_german_to_str(1.0), 'Ja')
+        self.assertEqual(tristate_german_to_str(0.5), 'Teilweise')
+        self.assertEqual(tristate_german_to_str(0.0), 'Nein')
+
+
+class TestOptTristateGerman(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(opt_tristate_german_from_str('Ja'), 1.0)
+        self.assertEqual(opt_tristate_german_from_str('Teilweise'), 0.5)
+        self.assertEqual(opt_tristate_german_from_str('Nein'), 0)
+        self.assertEqual(opt_tristate_german_from_str(''), None)
+        with self.assertRaises(ValueError):
+            opt_tristate_german_from_str('Vielleicht')
+
+    def test_to_str(self):
+        self.assertEqual(opt_tristate_german_to_str(1.0), 'Ja')
+        self.assertEqual(opt_tristate_german_to_str(0.5), 'Teilweise')
+        self.assertEqual(opt_tristate_german_to_str(0.0), 'Nein')
+        self.assertEqual(opt_tristate_german_to_str(None), '')
+
+
+class TestOptTristateGermanComment(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(opt_tristate_german_comment_from_str('Ja'), (1.0, None))
+        self.assertEqual(opt_tristate_german_comment_from_str('Teilweise'), (0.5, None))
+        self.assertEqual(opt_tristate_german_comment_from_str('Nein'), (0, None))
+        self.assertEqual(opt_tristate_german_comment_from_str('Teilweise (nur ganz oben nicht)'), (0.5, 'nur ganz oben nicht'))
+        self.assertEqual(opt_tristate_german_comment_from_str(''), (None, None))
+        with self.assertRaises(ValueError):
+            opt_tristate_german_from_str('Vielleicht')
+        with self.assertRaises(ValueError):
+            opt_tristate_german_from_str('(Ja)')
+
+    def test_to_str(self):
+        self.assertEqual(opt_tristate_german_comment_to_str((1.0, None)), 'Ja')
+        self.assertEqual(opt_tristate_german_comment_to_str((0.5, None)), 'Teilweise')
+        self.assertEqual(opt_tristate_german_comment_to_str((0.0, None)), 'Nein')
+        self.assertEqual(opt_tristate_german_comment_to_str((None, None)), '')
+
+
+class TestLonLat(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(lonlat_from_str('47.076207 N 11.453553 E'), LonLat(11.453553, 47.076207))
+        with self.assertRaises(ValueError):
+            lonlat_from_str('47.076207 N 11.453553')
+
+    def test_to_str(self):
+        self.assertEqual(lonlat_to_str(LonLat(11.453553, 47.076207)), '47.076207 N 11.453553 E')
+
+
+class TestValueCommentConverter(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(value_comment_from_str('abc (defg)'), ('abc', 'defg'))
+        self.assertEqual(value_comment_from_str('abc ()'), ('abc', ''))
+        self.assertEqual(value_comment_from_str('(def)'), ('', 'def'))
+        self.assertEqual(value_comment_from_str('ab((cd))'), ('ab', '(cd)'))
+        self.assertEqual(value_comment_from_str('ab((c(d)[(])))'), ('ab', '(c(d)[(]))'))
+        self.assertEqual(value_comment_from_str('ab((cd)'), ('ab(', 'cd'))
+        self.assertEqual(value_comment_from_str('abcd  (ef) '), ('abcd', 'ef'))
+        self.assertEqual(value_comment_from_str('abc', comment_optional=True), ('abc', ''))
+        with self.assertRaises(ValueError):
+            value_comment_from_str('abc (')
+        with self.assertRaises(ValueError):
+            value_comment_from_str('abc )')
+        with self.assertRaises(ValueError):
+            value_comment_from_str('abc (def)g')
+        with self.assertRaises(ValueError):
+            value_comment_from_str('abc (b))')
+        with self.assertRaises(ValueError):
+            value_comment_from_str('abc')
+
+    def test_to_str(self):
+        self.assertEqual(value_comment_to_str(('abc', 'defg')), 'abc (defg)')
+        self.assertEqual(value_comment_to_str(('abc', '')), 'abc ()')
+        self.assertEqual(value_comment_to_str(('', 'def')), '(def)')
+        self.assertEqual(value_comment_to_str(('ab', '(cd)')), 'ab ((cd))')
+        self.assertEqual(value_comment_to_str(('ab', '(c(d)[(]))')), 'ab ((c(d)[(])))')
+        self.assertEqual(value_comment_to_str(('ab(', 'cd')), 'ab( (cd)')
+        self.assertEqual(value_comment_to_str(('abcd', 'ef')), 'abcd (ef)')
+        self.assertEqual(value_comment_to_str(('abc', ''), comment_optional=True), 'abc')
+
+
+class TestNoGermanConverter(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(no_german_from_str('abc'), (True, 'abc'))
+        self.assertEqual(no_german_from_str('Nein'), (False, None))
+        with self.assertRaises(ValueError):
+            no_german_from_str('')
+
+    def test_to_str(self):
+        self.assertEqual(no_german_to_str((True, 'abc')), 'abc')
+        self.assertEqual(no_german_to_str((False, None)), 'Nein')
+
+
+class TestOptNoGerman(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(opt_no_german_from_str('abc'), (True, 'abc'))
+        self.assertEqual(opt_no_german_from_str('Nein'), (False, None))
+        self.assertEqual(opt_no_german_from_str(''), (None, None))
+
+    def test_to_str(self):
+        self.assertEqual(opt_no_german_to_str((True, 'abc')), 'abc')
+        self.assertEqual(opt_no_german_to_str((False, None)), 'Nein')
+        self.assertEqual(opt_no_german_to_str((None, None)), '')
+
+
+class TestLiftGermanValidator(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(lift_german_from_str(''), None)
+        self.assertEqual(lift_german_from_str('Nein'), [])
+        self.assertEqual(lift_german_from_str('Sessellift'), [('Sessellift', None)])
+        self.assertEqual(lift_german_from_str('Gondel (nur bis zur Hälfte)'), [('Gondel', 'nur bis zur Hälfte')])
+        self.assertEqual(lift_german_from_str('Sessellift; Taxi'), [('Sessellift', None), ('Taxi', None)])
+        self.assertEqual(lift_german_from_str('Sessellift (Wochenende); Taxi (6 Euro)'), [('Sessellift', 'Wochenende'), ('Taxi', '6 Euro')])
+
+    def test_to_str(self):
+        self.assertEqual(lift_german_to_str(None), '')
+        self.assertEqual(lift_german_to_str([]), 'Nein')
+        self.assertEqual(lift_german_to_str([('Sessellift', None)]), 'Sessellift')
+        self.assertEqual(lift_german_to_str([('Gondel', 'nur bis zur Hälfte')]), 'Gondel (nur bis zur Hälfte)')
+        self.assertEqual(lift_german_to_str([('Sessellift', None), ('Taxi', None)]), 'Sessellift; Taxi')
+        self.assertEqual(lift_german_to_str([('Sessellift', 'Wochenende'), ('Taxi', '6 Euro')]), 'Sessellift (Wochenende); Taxi (6 Euro)')
+
+
+class TestNightLightDays(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(nightlightdays_from_str(''), (None, None))
+        self.assertEqual(nightlightdays_from_str('2 (Mo, Di)'), (2, 'Mo, Di'))
+        self.assertEqual(nightlightdays_from_str('7'), (7, None))
+        self.assertEqual(nightlightdays_from_str('0'), (0, None))
+        self.assertEqual(nightlightdays_from_str('(keine Ahnung)'), (None, 'keine Ahnung'))
+        with self.assertRaises(ValueError):
+            nightlightdays_from_str('8')
+        with self.assertRaises(ValueError):
+            nightlightdays_from_str('5 (Montag')
+        with self.assertRaises(ValueError):
+            nightlightdays_from_str('5.2')
+
+    def test_to_str(self):
+        self.assertEqual(nightlightdays_to_str((None, None)), '')
+        self.assertEqual(nightlightdays_to_str((2, 'Mo, Di')), '2 (Mo, Di)')
+        self.assertEqual(nightlightdays_to_str((7, None)), '7')
+        self.assertEqual(nightlightdays_to_str((0, None)), '0')
+
+
+class TestSledRental(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(sledrental_from_str(''), None)
+        self.assertEqual(sledrental_from_str('Nein'), [])
+        self.assertEqual(sledrental_from_str('Talstation'), [('Talstation', None)])
+        self.assertEqual(sledrental_from_str('Talstation (unten)'), [('Talstation', 'unten')])
+        self.assertEqual(sledrental_from_str('Talstation (unten); Mittelstation'), [('Talstation', 'unten'), ('Mittelstation', None)])
+        with self.assertRaises(ValueError):
+            sledrental_from_str('(unten)')
+        with self.assertRaises(ValueError):
+            sledrental_from_str('Talstation (unten); ; Mittelstation')
+
+    def test_to_str(self):
+        self.assertEqual(sledrental_to_str(None), '')
+        self.assertEqual(sledrental_to_str([]), 'Nein')
+        self.assertEqual(sledrental_to_str([('Talstation', None)]), 'Talstation')
+        self.assertEqual(sledrental_to_str([('Talstation', 'unten')]), 'Talstation (unten)')
+        self.assertEqual(sledrental_to_str([('Talstation', 'unten'), ('Mittelstation', None)]), 'Talstation (unten); Mittelstation')
+
+
+class TestSingleCachet(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(single_cachet_german_from_str('Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'), ('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel'))
+        self.assertEqual(single_cachet_german_from_str('Tiroler Naturrodelbahn-Gütesiegel 2013 schwer'), ('Tiroler Naturrodelbahn-Gütesiegel', '2013', 'schwer'))
+        with self.assertRaises(ValueError):
+            single_cachet_german_from_str('')
+        with self.assertRaises(ValueError):
+            single_cachet_german_from_str('Salzburger Naturrodelbahn-Gütesiegel 2013 schwer')
+        with self.assertRaises(ValueError):
+            single_cachet_german_from_str('Tiroler Naturrodelbahn-Gütesiegel 4013 schwer')
+        with self.assertRaises(ValueError):
+            single_cachet_german_from_str('Tiroler Naturrodelbahn-Gütesiegel 13 schwer')
+        with self.assertRaises(ValueError):
+            single_cachet_german_from_str('Tiroler Naturrodelbahn-Gütesiegel 2013 schwerer')
+
+    def test_to_str(self):
+        self.assertEqual(single_cachet_german_to_str(('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')), 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel')
+        self.assertEqual(single_cachet_german_to_str(('Tiroler Naturrodelbahn-Gütesiegel', '2013', 'schwer')), 'Tiroler Naturrodelbahn-Gütesiegel 2013 schwer')
+
+
+class TestCachetGerman(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(cachet_german_from_str(''), None)
+        self.assertEqual(cachet_german_from_str('Nein'), [])
+        self.assertEqual(cachet_german_from_str('Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'), [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')])
+        self.assertEqual(cachet_german_from_str('Tiroler Naturrodelbahn-Gütesiegel 2013 schwer; Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'),
+            [('Tiroler Naturrodelbahn-Gütesiegel', '2013', 'schwer'), ('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')])
+        with self.assertRaises(ValueError):
+            cachet_german_from_str('Ja')
+        with self.assertRaises(ValueError):
+            cachet_german_from_str('Tiroler Naturrodelbahn-Gütesiegel 2013 schwer Tiroler Naturrodelbahn-Gütesiegel 2009 mittel')
+
+    def test_to_str(self):
+        self.assertEqual(cachet_german_to_str(None), '')
+        self.assertEqual(cachet_german_to_str([]), 'Nein')
+        self.assertEqual(cachet_german_to_str([('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]), 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel')
+        self.assertEqual(cachet_german_to_str([('Tiroler Naturrodelbahn-Gütesiegel', '2013', 'schwer'), ('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]),
+            'Tiroler Naturrodelbahn-Gütesiegel 2013 schwer; Tiroler Naturrodelbahn-Gütesiegel 2009 mittel')
+
+
+class TestUrl(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(url_from_str('http://www.winterrodeln.org/wiki/Arzler_Alm/'), 'http://www.winterrodeln.org/wiki/Arzler_Alm/')
+        self.assertEqual(url_from_str('http://www.winterrodeln.org/wiki/Nösslachhütte/'), 'http://www.winterrodeln.org/wiki/Nösslachhütte/')
+        self.assertEqual(url_from_str('https://www.winterrodeln.org/wiki/Nösslachhütte/'), 'https://www.winterrodeln.org/wiki/Nösslachhütte/')
+        with self.assertRaises(ValueError):
+            url_from_str('mailto:office@example.com')
+        with self.assertRaises(ValueError):
+            url_from_str('/wiki/Arzler_Alm/')
+
+    def test_to_str(self):
+        self.assertEqual(url_to_str('http://www.winterrodeln.org/wiki/Arzler_Alm/'), 'http://www.winterrodeln.org/wiki/Arzler_Alm/')
+        self.assertEqual(url_to_str('http://www.winterrodeln.org/wiki/Nösslachhütte/'), 'http://www.winterrodeln.org/wiki/Nösslachhütte/')
+        self.assertEqual(url_to_str('https://www.winterrodeln.org/wiki/Nösslachhütte/'), 'https://www.winterrodeln.org/wiki/Nösslachhütte/')
+
+
+class TestWebauskunft(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(webauskunft_from_str('http://www.example.com/current'), (True, 'http://www.example.com/current'))
+        self.assertEqual(webauskunft_from_str(''), (None, None))
+        self.assertEqual(webauskunft_from_str('Nein'), (False, None))
+
+    def test_to_str(self):
+        self.assertEqual(webauskunft_to_str((True, 'http://www.example.com/current')), 'http://www.example.com/current')
+        self.assertEqual(webauskunft_to_str((None, None)), '')
+        self.assertEqual(webauskunft_to_str((False, None)), 'Nein')
+
+
+class TestPhoneNumber(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(phone_number_from_str('+43-699-123456789'), '+43-699-123456789')
+        self.assertEqual(phone_number_from_str('+43-69945'), '+43-69945')
+        self.assertEqual(phone_number_from_str('+43-512-507-6418'), '+43-512-507-6418')
+        with self.assertRaises(ValueError):
+            phone_number_from_str('+43-')
+        with self.assertRaises(ValueError):
+            phone_number_from_str('0512123456789')
+
+    def test_to_str(self):
+        self.assertEqual(phone_number_to_str('+43-699-123456789'), '+43-699-123456789')
+        self.assertEqual(phone_number_to_str('+43-69945'), '+43-69945')
+        self.assertEqual(phone_number_to_str('+43-512-507-6418'), '+43-512-507-6418')
+
+
+class TestTelefonauskunft(unittest.TestCase):
+    def test_from_str(self):
+        self.assertEqual(telefonauskunft_from_str(''), None)
+        self.assertEqual(telefonauskunft_from_str('Nein'), [])
+        self.assertEqual(telefonauskunft_from_str('+43-512-123456 (untertags)'), [('+43-512-123456', 'untertags')])
+        self.assertEqual(telefonauskunft_from_str('+43-512-1234 (untertags); +43-664-123456 (Alm)'), [('+43-512-1234', 'untertags'), ('+43-664-123456', 'Alm')])
+        with self.assertRaises(ValueError):
+            telefonauskunft_from_str('+43-512-123456+ (untertags)')
+        with self.assertRaises(ValueError):
+            telefonauskunft_from_str('+43-512-123456')
+
+    def test_to_str(self):
+        self.assertEqual(telefonauskunft_to_str(None), '')
+        self.assertEqual(telefonauskunft_to_str([]), 'Nein')
+        self.assertEqual(telefonauskunft_to_str([('+43-512-123456', 'untertags')]), '+43-512-123456 (untertags)')
+        self.assertEqual(telefonauskunft_to_str([('+43-512-1234', 'untertags'), ('+43-664-123456', 'Alm')]), '+43-512-1234 (untertags); +43-664-123456 (Alm)')
+
+
+class TestBox(unittest.TestCase):
+    def test_from_str(self):
+        value = '{{MyTemplate|apple=2|banana=5}}'
+        converter_dict = OrderedDict([('apple', opt_int_converter), ('banana', opt_int_converter)])
+        result = box_from_str(value, 'MyTemplate', converter_dict)
+        self.assertEqual(result['apple'], 2)
+        self.assertEqual(result['banana'], 5)
+
+        value = '{{MyTemplate\n | apple = 2 \n| banana = 5 }}'
+        result = box_from_str(value, 'MyTemplate', converter_dict)
+        self.assertEqual(result['apple'], 2)
+        self.assertEqual(result['banana'], 5)
+
+        with self.assertRaises(ValueError):
+            box_from_str(value, 'myTemplate', converter_dict)
+        with self.assertRaises(ValueError):
+            value = '{{MyTemplate|apple=2|banana=five}}'
+            box_from_str(value, 'MyTemplate', converter_dict)
+        with self.assertRaises(ValueError):
+            value = '{{MyTemplate|apple=2}}'
+            box_from_str(value, 'MyTemplate', converter_dict)
+        with self.assertRaises(ValueError):
+            value = '{{MyTemplate|apple=2|banana=5|cherry=6}}'
+            box_from_str(value, 'MyTemplate', converter_dict)
+
+    def test_to_str(self):
+        value = OrderedDict([('apple', 2), ('banana', 5)])
+        converter_dict = OrderedDict([('apple', opt_int_converter), ('banana', opt_int_converter)])
+        result = box_to_str(value, 'MyTemplate', converter_dict)
+        self.assertEqual(result, '{{MyTemplate|apple=2|banana=5}}')
+
+
+class TestRodelbahnbox(unittest.TestCase):
+    def setUp(self):
+        self.maxDiff = None
+        self.value = \
+'''{{Rodelbahnbox
+| Position             = 46.807218 N 12.806522 E
+| Position oben        = 46.799014 N 12.818658 E
+| Höhe oben            = 1046
+| Position unten       =
+| Höhe unten           =
+| Länge                = 3500
+| Schwierigkeit        = mittel
+| Lawinen              = kaum
+| Betreiber            = Bringungsgemeinschaft Kreithof-Dolomitenhütte
+| Öffentliche Anreise  = Schlecht
+| Aufstieg möglich     = Ja
+| Aufstieg getrennt    = Teilweise
+| Gehzeit              = 75
+| Aufstiegshilfe       = Taxi; Sonstige (PKW bis Kreithof)
+| Beleuchtungsanlage   = Ja
+| Beleuchtungstage     = 7
+| Rodelverleih         = Nein
+| Gütesiegel           = Tiroler Naturrodelbahn-Gütesiegel 2009 mittel
+| Webauskunft          = http://www.lienzerdolomiten.info/at/tobogorpt.html
+| Telefonauskunft      = +43-664-2253782 (Dolomitenhütte)
+| Bild                 = Dolomitenrodelbahn Tristach 2011-12-22 oberer Bereich.jpg
+| In Übersichtskarte   = Ja
+| Forumid              = 139
+}}'''
+
+    def test_from_str(self):
+        value = rodelbahnbox_from_str(self.value)
+        self.assertEqual(value['Position'], LonLat(12.806522, 46.807218))
+        self.assertEqual(value['Position oben'], LonLat(12.818658, 46.799014))
+        self.assertEqual(value['Höhe oben'], 1046)
+        self.assertEqual(value['Position unten'], LonLat(None, None))
+        self.assertEqual(value['Höhe unten'], None)
+        self.assertEqual(value['Länge'], 3500)
+        self.assertEqual(value['Schwierigkeit'], 2)
+        self.assertEqual(value['Lawinen'], 1)
+        self.assertEqual(value['Betreiber'], 'Bringungsgemeinschaft Kreithof-Dolomitenhütte')
+        self.assertEqual(value['Öffentliche Anreise'], 4)
+        self.assertEqual(value['Aufstieg möglich'], True)
+        self.assertEqual(value['Aufstieg getrennt'], (0.5, None))
+        self.assertEqual(value['Gehzeit'], 75)
+        self.assertEqual(value['Aufstiegshilfe'], [('Taxi', None), ('Sonstige', 'PKW bis Kreithof')])
+        self.assertEqual(value['Beleuchtungsanlage'], (1.0, None))
+        self.assertEqual(value['Beleuchtungstage'], (7, None))
+        self.assertEqual(value['Rodelverleih'], [])
+        self.assertEqual(value['Gütesiegel'], [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')])
+        self.assertEqual(value['Webauskunft'], (True, 'http://www.lienzerdolomiten.info/at/tobogorpt.html'))
+        self.assertEqual(value['Telefonauskunft'], [('+43-664-2253782', 'Dolomitenhütte')])
+        self.assertEqual(value['Bild'], 'Dolomitenrodelbahn Tristach 2011-12-22 oberer Bereich.jpg')
+        self.assertEqual(value['In Übersichtskarte'], True)
+        self.assertEqual(value['Forumid'], 139)
+
+    def test_to_str(self):
+        value = OrderedDict([
+            ('Position', LonLat(12.806522, 46.807218)),
+            ('Position oben', LonLat(12.818658, 46.799014)),
+            ('Höhe oben', 1046),
+            ('Position unten', LonLat(None, None)),
+            ('Höhe unten', None),
+            ('Länge', 3500),
+            ('Schwierigkeit', 2),
+            ('Lawinen', 1),
+            ('Betreiber', 'Bringungsgemeinschaft Kreithof-Dolomitenhütte'),
+            ('Öffentliche Anreise', 4),
+            ('Aufstieg möglich', True),
+            ('Aufstieg getrennt', (0.5, None)),
+            ('Gehzeit', 75),
+            ('Aufstiegshilfe', [('Taxi', None), ('Sonstige', 'PKW bis Kreithof')]),
+            ('Beleuchtungsanlage', (1.0, None)),
+            ('Beleuchtungstage', (7, None)),
+            ('Rodelverleih', []),
+            ('Gütesiegel', [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]),
+            ('Webauskunft', (True, 'http://www.lienzerdolomiten.info/at/tobogorpt.html')),
+            ('Telefonauskunft', [('+43-664-2253782', 'Dolomitenhütte')]),
+            ('Bild', 'Dolomitenrodelbahn Tristach 2011-12-22 oberer Bereich.jpg'),
+            ('In Übersichtskarte', True),
+            ('Forumid', 139)])
+        self.assertEqual(rodelbahnbox_to_str(value), self.value)
 
 
 class TestWrValidators(unittest.TestCase):
-    def test_OrderedSchema(self):
-        v = wrpylib.wrvalidators.OrderedSchema()
-        v.pre_validators = [formencode.Validator()]
-        v.chained_validators = [formencode.Validator()]
-        v.add_field('c', formencode.Validator())
-        v.add_field('b', formencode.Validator())
-        v.add_field('a', formencode.Validator())
-        v.add_field('d', formencode.Validator())
-        other = collections.OrderedDict([('d', 'd'), ('b', 'b'), ('a', 'a'), ('c', 'c')])
-        python = v.to_python(other)
-        assert list(python.keys()) == list(other.keys())
-        assert list(python.values()) == list(other.values())
-        other2 = v.from_python(python)
-        assert list(other.keys()) == list(other2.keys())
-        assert list(other.values()) == list(other2.values())
-
-
-    def test_NoneValidator(self):
-        v =  wrpylib.wrvalidators.NoneValidator(wrpylib.wrvalidators.Unicode())
-        assert v.to_python('') == None
-        assert v.from_python(None) == ''
-
-
-    # test_NeinValidator
-
-
-    # test_Unicode
-
-
-    # test_UnicodeNone
-
-
-    # test_Unsigned
-
-
-    def test_UnsignedNone(self):
-        v = wrpylib.wrvalidators.UnsignedNone()
-        assert v.to_python('42') == 42
-        assert v.to_python('') == None
-        assert v.from_python(42) == '42'
-        assert v.from_python(None) == ''
-
-
-    # test_UnsignedNeinNone
-
-
-    # test_Loop
-
-
-    # test_DictValidator
-
-
-    # test_GermanBoolNone
-
-
-    def test_GermanTristateTuple(self):
-        v = wrpylib.wrvalidators.GermanTristateTuple()
-        assert v.to_python('') == (None, None)
-        assert v.to_python('Ja') == (True, False)
-        assert v.to_python('Nein') == (False, True)
-        assert v.to_python('Teilweise') == (True, True)
-        assert v.from_python((None, None)) == ''
-        assert v.from_python((False, True)) == 'Nein'
-        assert v.from_python((True, False)) == 'Ja'
-        assert v.from_python((True, True)) == 'Teilweise'
-
-
-    def test_GermanTristateFloat(self):
-        v = wrpylib.wrvalidators.GermanTristateFloat()
-        assert v.to_python('') == None
-        assert v.to_python('Ja') == 1.0
-        assert v.to_python('Nein') == 0.0
-        assert v.to_python('Teilweise') == 0.5
-        assert v.from_python(None) == ''
-        assert v.from_python(0.0) == 'Nein'
-        assert v.from_python(1.0) == 'Ja'
-        assert v.from_python(0.5) == 'Teilweise'
-
-
-    # test_ValueComment
-
-
-    # test_SemicolonList
-
-
-    def test_ValueCommentList(self):
-        v = wrpylib.wrvalidators.ValueCommentList()
-        assert v.to_python('abc') == [('abc', None)]
-        assert v.to_python('abc def') == [('abc def', None)]
-        assert v.to_python('value (comment)') == [('value', 'comment')]
-        assert v.to_python('value (comment)') == [('value', 'comment')]
-        assert v.to_python('value1 (comment); value2') == [('value1', 'comment'), ('value2', None)]
-        assert v.to_python('value1 (comment1); value2; value3 (comment3)') == [('value1', 'comment1'), ('value2', None), ('value3', 'comment3')]
-        assert v.to_python('value1 (comment1); [[link (linkcomment)]] (not easy)') == [('value1', 'comment1'), ('[[link (linkcomment)]]', 'not easy')]
-
-
-    # test_GenericDateTime
-
-
-    # test_DateTimeNoSec
-
-
-    # test_DateNone
-
-
-    # test_Geo
-
-
-    def test_GeoNone(self):
-        coord = '47.076207 N 11.453553 E'
-        v = wrpylib.wrvalidators.GeoNone()
-        (lat, lon) = v.to_python(coord)
-        assert lat == 47.076207
-        assert lon == 11.453553
-        assert v.to_python('') == (None, None)
-
-        assert v.from_python((lat, lon)) == coord
-        assert v.from_python((None, None)) == ''
-
-
-    # test_MultiGeo
-
-
-    # test_AustrianPhoneNumber
-
-
-    # test_AustrianPhoneNumberNone
-
-
-    # test_AustrianPhoneNumberCommentLoop
-
-
-    # test_GermanDifficulty
-
-
-    # test_GermanAvalanches
-
-
-    def test_GermanPublicTransport(self):
-        v = wrpylib.wrvalidators.GermanPublicTransport()
-        assert v.to_python('') is None
-        assert v.to_python('Sehr gut') == 1
-        assert v.to_python('Gut') == 2
-        assert v.to_python('Mittelmäßig') == 3
-        assert v.to_python('Schlecht') == 4
-        assert v.to_python('Nein') == 5
-        assert v.to_python('Ja') == 6
-
-        assert v.from_python(None) == ''
-        assert v.from_python(1) == 'Sehr gut'
-        assert v.from_python(2) == 'Gut'
-        assert v.from_python(3) == 'Mittelmäßig'
-        assert v.from_python(4) == 'Schlecht'
-        assert v.from_python(5) == 'Nein'
-        assert v.from_python(6) == 'Ja'
-        assert v.from_python(1) == 'Sehr gut'
-
-
-    # test_GermanTristateFloatComment
-
-
-    # test_UnsignedCommentNone
-
-
-    # test_GermanCachet
-
-
-    # test_url
-
-
-    def test_UrlNeinNone(self):
-        v = wrpylib.wrvalidators.UrlNeinNone()
-        assert v.to_python('') == None
-        assert v.to_python('Nein') == 'Nein'
-        assert v.to_python('http://www.höttingeralm.at') == 'http://www.höttingeralm.at'
-        assert v.from_python(None) == ''
-        assert v.from_python('Nein') == 'Nein'
-        assert v.from_python('http://www.höttingeralm.at') == 'http://www.höttingeralm.at'
-
-
     def test_ValueCommentListNeinLoopNone(self):
         v = wrpylib.wrvalidators.ValueCommentListNeinLoopNone()
         assert v.to_python('') == None
@@ -197,9 +504,6 @@ class TestWrValidators(unittest.TestCase):
         assert v.from_python('T-Mobile (gut); A1') == 'T-Mobile (gut); A1'
 
 
-    # test_PhoneNumber
-
-
     def test_PhoneCommentListNeinLoopNone(self):
         v = wrpylib.wrvalidators.PhoneCommentListNeinLoopNone(comments_are_optional=True)
         assert v.to_python('') == None
@@ -246,12 +550,6 @@ class TestWrValidators(unittest.TestCase):
         assert v.from_python(testvalue) == testvalue
 
 
-    # test_WikiPage
-
-
-    # test_WikiPageList
-
-
     def test_WikiPageListLoopNone(self):
         v = wrpylib.wrvalidators.WikiPageListLoopNone()
         assert v.to_python('') == None
@@ -260,74 +558,6 @@ class TestWrValidators(unittest.TestCase):
         assert v.from_python('[[Birgitzer Alm]]; [[Kemater Alm]]') == '[[Birgitzer Alm]]; [[Kemater Alm]]'
 
 
-    # test_TupleSecondValidator
-
-
-    def test_BoolUnicodeTupleValidator(self):
-        v = wrpylib.wrvalidators.BoolUnicodeTupleValidator()
-        assert v.to_python('') == (None, None)
-        assert v.to_python('Nein') == (False, None)
-        assert v.to_python('any text') == (True, 'any text')
-        assert v.from_python((None, None)) == ''
-        assert v.from_python((False, None)) == 'Nein'
-        assert v.from_python((True, 'any text')) == 'any text'
-
-
-
-
-    def test_GermanLift(self):
-        v = wrpylib.wrvalidators.GermanLift()
-        assert v.to_python('') == (None, None)
-        assert v.to_python('Nein') == (False, None)
-        assert v.to_python('Sessellift (4 Euro)') == (True, 'Sessellift (4 Euro)')
-        assert v.from_python((None, None)) == ''
-        assert v.from_python((False, None)) == 'Nein'
-        assert v.from_python((True, 'Sessellift (4 Euro)')) == 'Sessellift (4 Euro)'
-
-
-    def test_SledRental(self):
-        v = wrpylib.wrvalidators.SledRental()
-        assert v.to_python('') == (None, None)
-        assert v.to_python('Nein') == (False, None)
-        assert v.to_python('Ja') == (True, 'Ja')
-        assert v.to_python('Talstation (nur mit Ticket); Schneealm') == (True, 'Talstation (nur mit Ticket); Schneealm')
-        assert v.from_python((None, None)) == ''
-        assert v.from_python((False, None)) == 'Nein'
-        assert v.from_python((True, 'Talstation (nur mit Ticket); Schneealm')) == 'Talstation (nur mit Ticket); Schneealm'
-        assert v.from_python((True, 'Ja')) == 'Ja'
-
-
-    def test_RodelbahnboxDictValidator(self):
-        v = wrpylib.wrvalidators.RodelbahnboxDictValidator()
-        other = collections.OrderedDict([
-            ('Position', '47.309820 N 9.986508 E'),
-            ('Position oben', ''),
-            ('Höhe oben', '1244'),
-            ('Position unten', ''),
-            ('Höhe unten', '806'),
-            ('Länge', '5045'),
-            ('Schwierigkeit', ''),
-            ('Lawinen', 'gelegentlich'),
-            ('Betreiber', ''),
-            ('Öffentliche Anreise', 'Ja'),
-            ('Aufstieg möglich', 'Ja'),
-            ('Aufstieg getrennt', 'Nein'),
-            ('Gehzeit', '105'),
-            ('Aufstiegshilfe', 'Nein'),
-            ('Beleuchtungsanlage', 'Nein'),
-            ('Beleuchtungstage', ''),
-            ('Rodelverleih', 'Ja'),
-            ('Gütesiegel', ''),
-            ('Webauskunft', ''),
-            ('Telefonauskunft', '+43-664-1808482 (Bergkristallhütte)'),
-            ('Bild', 'Rodelbahn Bergkristallhütte 2009-03-03.jpg'),
-            ('In Übersichtskarte', 'Ja'),
-            ('Forumid', '72')])
-        python = v.to_python(other, None)
-        other2 = v.from_python(python, None)
-        assert other == other2
-
-
     def test_GasthausboxDictValidator(self):
         v = wrpylib.wrvalidators.GasthausboxDictValidator()
         other = collections.OrderedDict([
index 572b30cfc3fed4258e585f18a913946ade9dc7b5..dd2d6bf6d2fb6f96f5a56bad9ae0a7edad3344c1 100644 (file)
 #!/usr/bin/python3.4
-# -*- coding: iso-8859-15 -*-
+# coding=utf-8
 # $Id$
 # $HeadURL$
-"""This module contains general functions that help parsing the mediawiki markup.
-I looked for an already existing MediaWiki parser in Python but I didn't find anything 
-that convinced me. However, here are the links:
+"""For parsing MediaWiki text, we rely on the package mwparserfromhell (https://github.com/earwig/mwparserfromhell).
+This module just contains a few additional useful functions.
 
+Other Python MediaWiki parsers:
 * py-wikimarkup https://github.com/dcramer/py-wikimarkup
 * mwlib http://code.pediapress.com/wiki/wiki
+* https://www.mediawiki.org/wiki/Alternative_parsers
 """
-import re
-import xml.etree.ElementTree
-import collections
-import formencode
-
 
 class ParseError(RuntimeError):
     """Exception used by some of the functions"""
     pass
 
 
-def find_template(wikitext, template_title):
-    """Returns the tuple (start, end) of the first occurence of the template '{{template ...}} within wikitext'.
-    (None, None) is returned if the template is not found.
-    If you are sure that the wikitext contains the template, the template could be extracted like follows:
-
-    >>> wikitext = u'This is a {{Color|red|red text}} template.'
-    >>> start, end = find_template(wikitext, u'Color')
-    >>> print wikitext[start:end]
-    {{Color|red|red text}}
-
-    or just:
-
-    >>> print wikitext.__getslice__(*find_template(wikitext, u'Color'))
-    {{Color|red|red text}}
-
-    The search is done with regular expression. It gives wrong results when parsing a template
-    containing the characters "}}"
-
-    :param wikitext: The text (preferalbe unicode) that has the template in it.
-    :param template_title: The page title of the template with or without namespace (but as in the wikitext).
-    :return: 
-        (start, end) of the first occurence with start >= 0 and end > start.
-        (None, None) if the template is not found.
-    """ 
-    match = re.search("\{\{" + template_title + "\s*(\|[^\}]*)?\}\}", wikitext,  re.DOTALL)
-    if match is None: return None, None
-    return match.start(), match.end()
-
-
-class TemplateValidator(formencode.FancyValidator):
-    def __init__(self, strip=True, as_table=False, as_table_keylen=None):
-        """Validates a MediaWiki template, e.g. {{Color|red}}
-        :param stip: If strip is True, the title, and the parameter keys and values are stripped in to_python.
-        :param as_table: formats the returned template in one row for each parameter
-        :param as_table_keylen: length of the key field for from_python. None for "automatic"."""
-        self.strip = (lambda s: s.strip()) if strip else (lambda s: s)
-        self.as_table = as_table
-        self.as_table_keylen = as_table_keylen
-
-    def to_python(self, value, state=None):
-        """Takes a template, like u'{{Color|red|text=Any text}}' and translates it to a Python tuple
-        (title, anonym_params, named_params) where title is the template title,
-        anonym_params is a list of anonymous parameters and named_params is a OrderedDict
-        of named parameters. Whitespace of the parameters is stripped."""
-        if not value.startswith('{{'):
-            raise formencode.Invalid('Template does not start with "{{"', value, state)
-        if not value.endswith('}}'):
-            raise formencode.Invalid('Template does not end with "}}"', value, state)
-        parts = value[2:-2].split('|')
-
-        # template name
-        title = self.strip(parts[0])
-        if len(title) == 0:
-            raise formencode.Invalid('Empty template tilte.', value, state)
-        del parts[0]
-
-        # anonymous parameters
-        anonym_params = []
-        while len(parts) > 0:
-            equalsign_pos = parts[0].find('=')
-            if equalsign_pos >= 0: break # named parameter
-            anonym_params.append(self.strip(parts[0]))
-            del parts[0]
-
-        # named or numbered parameters
-        named_params = collections.OrderedDict()
-        while len(parts) > 0:
-            equalsign_pos = parts[0].find('=')
-            if equalsign_pos < 0:
-                raise formencode.Invalid('Anonymous parameter after named parameter.', value, state)
-            key, sep, value = parts[0].partition('=')
-            key = self.strip(key)
-            if len(key) == 0:
-                raise formencode.Invalid('Empty key.', value, state)
-            if key in named_params:
-                raise formencode.Invalid('Duplicate key: "{0}"'.format(key), value, state)
-            named_params[key] = self.strip(value)
-            del parts[0]
-
-        return title, anonym_params, named_params
-
-    def from_python(self, value, state=None):
-        """Formats a MediaWiki template.
-        value is a tuple: (title, anonym_params, named_params)
-        where title is the template title, anonym_params is a list of anonymous parameters and
-        named_params is a dict or OrderedDict of named parameters."""
-        title, anonym_params, named_params = value
-        pipe_char, equal_char, end_char = ('\n| ', ' = ', '\n}}') if self.as_table else ('|', '=', '}}')
-        parts = ["{{" + title]
-        parts += anonym_params
-        as_table_keylen = self.as_table_keylen
-        if self.as_table and as_table_keylen is None:
-            as_table_keylen = max(list(map(len, iter(named_params.keys()))))
-        for k, v in named_params.items():
-            if self.as_table:
-                k = k.ljust(as_table_keylen)
-                parts.append((k + equal_char + v).rstrip())
-            else:
-                parts.append(k + equal_char + v)
-        return pipe_char.join(parts) + end_char
-
-
-def split_template(template):
-    """Deprecated legacy function.
-
-    Takes a template, like u'{{Color|red|text=Any text}}' and translates it to a Python tuple
-    (template_title, parameters) where parameters is a Python dictionary {u'1': u'red', u'text'=u'Any text'}.
-    Anonymous parameters get integer keys (converted to unicode) starting with 1 
-    like in MediaWiki, named parameters are unicode strings.
-    Whitespace is stripped.
-    If an unexpected format is encountered, a ValueError is raised."""
-    try:
-        title, anonym_params, named_params = TemplateValidator().to_python(template)
-        parameters = dict(named_params)
-        for i in range(len(anonym_params)):
-            parameters[str(i+1)] = anonym_params[i]
-    except formencode.Invalid as e:
-        raise ValueError(e[0])
-    return title, parameters
-
-
-def create_template(template_title, anonym_params=[], named_param_keys=[], named_param_values=[], as_table=False, as_table_keylen=None):
-    """Deprecated legacy function.
-
-    Formats a MediaWiki template.
-    :param template_title: Unicode string with the template name
-    :param anonym_params: list with parameters without keys
-    :param named_param_keys: list with keys of named parameters
-    :param named_param_values: list with values of named parameters, corresponding to named_param_keys.
-    :param as_table: formats the returned template in one row for each parameter
-    :param as_table_keylen: length of the key field. None for "automatic".
-    :return: unicode template"""
-    named_params = collections.OrderedDict(list(zip(named_param_keys, named_param_values)))
-    return TemplateValidator(as_table=as_table, as_table_keylen=as_table_keylen).from_python((template_title, anonym_params, named_params))
+def template_to_table(template, keylen=None):
+    """Reformat the given template to be tabular.
 
+    >>> template
+    {{foo|bar|bazz=7}}
+    >>> template_to_table(template)
+    {{foo
+    | bar
+    | bazz = 7
+    }}
 
-def find_tag(wikitext, tagname, pos=0):
-    """Returns position information of the first occurence of the tag '<tag ...>...</tag>'
-    or '<tag ... />'.
-    If you are sure that the wikitext contains the tag, the tag could be extracted like follows:
-
-    >>> wikitext = u'This is a <tag>mytag</tag> tag.'
-    >>> start, content, endtag, end = find_template(wikitext, u'tag')
-    >>> print wikitext[start:end]
-    <tag>mytag</tag>
-
-    :param wikitext: The text (preferalbe unicode) that has the template in it.
-    :param tagname: Name of the tag, e.g. u'tag' for <tag>.
-    :param pos: position within wikitext to start searching the tag.
-    :return:
-        (start, content, endtag, end). start is the position of '<' of the tag,
-        content is the beginning of the content (after '>'), enttag is the
-        beginning of the end tag ('</') and end is one position after the end tag.
-        For single tags, (start, None, None, end) is returned.
-        If the tag is not found (or only the start tag is present,
-        (None, None, None, None) is returned.
+    :param keylen: length of the keys or None for automatic determination
     """
-    # Find start tag
-    regexp_starttag = re.compile("<{0}.*?(/?)>".format(tagname), re.DOTALL)
-    match_starttag = regexp_starttag.search(wikitext, pos)
-    if match_starttag is None:
-        return None, None, None, None
-
-    # does the tag have content?
-    if len(match_starttag.group(1)) == 1: # group(1) is either '' or '/'.
-        # single tag
-        return match_starttag.start(), None, None, match_starttag.end()
-
-    # tag with content
-    regexp_endtag = re.compile('</{0}>'.format(tagname), re.DOTALL)
-    match_endtag = regexp_endtag.search(wikitext, match_starttag.end())
-    if match_endtag is None:
-        # No closing tag - error in wikitext
-        return None, None, None, None
-    return match_starttag.start(), match_starttag.end(), match_endtag.start(), match_endtag.end()
-
-
-def parse_googlemap(wikitext):
-    """Parses the (unicode) u'<googlemap ...>content</googlemap>' of the googlemap extension.
-    If wikitext does not contain the <googlemap> tag or if the <googlemap> tag contains
-    invalid formatted lines, a ParseError is raised.
-    Use find_tag(wikitext, 'googlemap') to find the googlemap tag within an arbitrary
-    wikitext before using this function.
-
-    :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: The tuple (attributes, coords, paths) is returned.
-        attributes is a dict that contains the attribues that are present
-        (e.g. lon, lat, zoom, width, height) converted to float (lon, lat) or int.
-        coords is a list of (lon, lat, symbol, title) tuples.
-        paths is a list of (style, coords) tuples.
-        coords is again a list of (lon, lat, symbol, title) tuples."""
-
-    def is_coord(line):
-        """Returns True if the line contains a coordinate."""
-        match = re.search('[0-9]{1,2}\.[0-9]+, ?[0-9]{1,2}\.[0-9]+', line)
-        return not match is None
-
-    def is_path(line):
-        """Returns True if the line contains a path style definition."""
-        match = re.match('[0-9]#[0-9a-fA-F]{8}', line)
-        return not match is None
-
-    def parse_coord(line):
-        """Returns (lon, lat, symbol, title). If symbol or text is not present, None is returned."""
-        match = re.match('\(([^)]+)\) ?([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+), ?(.*)', line)
-        if not match is None: return (float(match.group(3)), float(match.group(2)), match.group(1), match.group(4))
-        match = re.match('\(([^)]+)\) ?([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+)', line)
-        if not match is None: return (float(match.group(3)), float(match.group(2)), match.group(1), None)
-        match = re.match('([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+), ?(.*)', line)
-        if not match is None: return (float(match.group(2)), float(match.group(1)), None, match.group(3))
-        match = re.match('([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+)', line)
-        if not match is None: return (float(match.group(2)), float(match.group(1)), None, None)
-        return ParseError('Could not parse line ' + line)
-
-    start, content, endtag, end = find_tag(wikitext, 'googlemap')
-    if start is None:
-        raise ParseError('<googlemap> tag not found.')
-    if content is None:
-        xml_only = wikitext[start:endtag]
-    else:
-        xml_only = wikitext[start:content]+wikitext[endtag:end]
-        
-    try:
-        gm = xml.etree.ElementTree.XML(xml_only.encode('UTF8'))
-    except xml.etree.ElementTree.ParseError as e:
-        row, column = e.position
-        raise ParseError("XML parse error in <googlemap ...>.")
-
-    # parse attributes
-    attributes = {}
-    try:
-        for key in ['lon', 'lat']:
-            if gm.get(key) is not None:
-                attributes[key] = float(gm.get(key))
-        for key in ['zoom', 'width', 'height']:
-            if gm.get(key) is not None:
-                attributes[key] = int(gm.get(key))
-    except ValueError as error:
-        raise ParseError('Error at parsing attribute {0} of <googlemap>: {1}'.format(key, str(error)))
-
-    # parse points and lines
-    coords = []
-    paths = []
-    lines = wikitext[content:endtag].split("\n")
-    i = 0
-    while i < len(lines):
-        line = lines[i].strip()
-        i += 1
-
-        # Skip whitespace
-        if len(line) == 0: continue
-
-        # Handle a path
-        if is_path(line):
-            match = re.match('([0-9]#[0-9a-fA-F]{8})', line)
-            style =  match.group(1)
-            local_coords = []
-            while i < len(lines):
-                line = lines[i].strip()
-                i += 1
-                if is_path(line):
-                    i -= 1
-                    break
-                if is_coord(line):
-                    lon, lat, symbol, title = parse_coord(line)
-                    local_coords.append((lon, lat, symbol, title))
-            paths.append((style, local_coords))
-            continue
-
-        # Handle a coordinate
-        if is_coord(line):
-            lon, lat, symbol, title = parse_coord(line)
-            while i < len(lines):
-                line = lines[i].strip()
-                i += 1
-                if is_path(line) or is_coord(line):
-                    i -= 1
-                    break
-                if len(line) > 0 and title is None: title = line
-            coords.append((lon, lat, symbol, title))
-            continue
-
-        raise ParseError('Unknown line syntax: ' + line)
-
-    return (attributes, coords, paths)
-
+    if keylen is None:
+        shown_keys = [len(param.name.strip()) for param in template.params if param.showkey]
+        keylen = max(shown_keys) if shown_keys else 0
+    template.name = '{}\n'.format(template.name.strip())
+    for param in template.params:
+        if param.showkey:
+            param.name = ' {{:{}}} '.format(keylen).format(param.name.strip())
+        value = param.value.strip()
+        if len(value) > 0:
+            param.value = ' {}\n'.format(value)
+        else:
+            param.value = '\n'
index f626f2b42476d0128351d2b1643f19fcd43c45e9..f2bac595cc638691eb9386d48ef9efcba264d3a0 100644 (file)
@@ -223,24 +223,19 @@ def inn_to_gasthausbox(inn):
     return GasthausboxValidator().from_python(inn)
 
 
-def find_template_latlon_ele(wikitext, template_title):
-    """Finds the first occurance of the '{{template_title|47.076207 N 11.453553 E|1890}}' template
-    and returns the tuple (start, end, lat, lon, ele) or (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, 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['1'].strip())
-    ele = wrpylib.wrvalidators.UnsignedNone().to_python(params['2'].strip())
-    return start, end, lat, lon, ele
+def split_template_latlon_ele(template):
+    """Template is a mwparserfromhell.nodes.template.Template instance. Returns (latlon, ele)."""
+    latlon = opt_geostr_to_lat_lon(template.params[1].strip())
+    ele = opt_intstr_to_int(template.params[2].strip())
+    return latlon, ele
 
 
-def create_template_latlon_ele(template_title, lat, lon, ele):
-    geo = wrpylib.wrvalidators.GeoNone().from_python((latlon))
+def create_template_latlon_ele(template_name, latlon, ele):
+    geo = wrpylib.wrvalidators.GeoNone().from_python((latlon))
     if len(geo) == 0: geo = ' '
     ele = wrpylib.wrvalidators.UnsignedNone().from_python(ele)
     if len(ele) == 0: ele = ' '
-    return wrpylib.mwmarkup.create_template(template_title, [geo, ele])
+    return wrpylib.mwmarkup.create_template(template_name, [geo, ele])
 
 
 def find_template_PositionOben(wikitext):
@@ -332,68 +327,6 @@ def find_template_Haltestelle(wikitext):
     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_templates(wikitext, find_template_Haltestelle)
-    Returns an empty list if the template was not found at all.
-    """
-    results = []
-    result = find_func(wikitext)
-    start, end = result[:2]
-    while start is not None:
-        results.append(result)
-        result = find_func(wikitext[end:])
-        if result[0] is None:
-            start = None
-        else:
-            start = result[0] + end
-            end  += result[1]
-            result = (start, end) + result[2:]
-    return results
-
-
-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)
-    """
-    json_features = []
-
-    # point
-    for point in coords:
-        lon, lat, symbol, title = point
-        properties = {'type': 'punkt' if symbol is None else symbol.lower()}
-        if title is not None: properties['name'] = title
-        json_features.append({
-            'type': 'Feature',
-            'geometry': {'type': 'Point', 'coordinates': [lon, lat]},
-            'properties': properties})
-        
-    # path
-    for path in paths:
-        style, entries = path
-        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',
-            'geometry': {
-                'type': 'LineString',
-                'coordinates': [[lon, lat] for lon, lat, symbol, title in entries]},
-            'properties': properties})
-
-    geojson = {
-            'type': 'FeatureCollection',
-            'features': json_features,
-            'properties': attributes}
-    return geojson
-
-
 def parse_wrmap_coordinates(coords):
     '''gets a string coordinates and returns an array of lon/lat coordinate pairs, e.g.
     47.12 N 11.87 E
index d3114550dd9a67d12c01b94241e06c004d703066..518234556e7af52f8ade0c2b167fdc1a1548a681 100644 (file)
@@ -2,17 +2,24 @@
 # -*- coding: iso-8859-15 -*-
 # $Id$
 # $HeadURL$
-"""This file contains "validators" that convert between string and python (database) representation
-of properties used in the "Rodelbahnbox" and "Gasthausbox".
-The "to_python" method has to get a unicode argument.
+"""
+A converter is a Python variable (may be a class, class instance or anything else) that has the member
+functions from_str and to_str. From string takes a string "from the outside", checks it and returns a Python variable
+representing that value in Python. It reports error by raising ValueError. to_str does the opposite, however, it
+can assume that the value it has to convert to a string is valid. If it gets an invalid value, the behavior is
+undefined.
 """
 import datetime
+import urllib.parse
 import re
 import xml.dom.minidom as minidom
 from xml.parsers.expat import ExpatError
-import collections
+from collections import OrderedDict, namedtuple
+
+import mwparserfromhell
 import formencode
 import formencode.national
+from wrpylib.mwmarkup import template_to_table
 
 
 class OrderedSchema(formencode.Schema):
@@ -25,7 +32,7 @@ class OrderedSchema(formencode.Schema):
         self.chained_validators = []
         try:
             result = formencode.Schema._convert_to_python(self, value, state)
-            ordered_result = collections.OrderedDict()
+            ordered_result = OrderedDict()
             for key in value.keys():
                 ordered_result[key] = result[key]
             for validator in chained_validators:
@@ -50,7 +57,7 @@ class OrderedSchema(formencode.Schema):
         # apply original _convert_from_python method
         try:
             result = formencode.Schema._convert_from_python(self, value, state)
-            ordered_result = collections.OrderedDict()
+            ordered_result = OrderedDict()
             for key in value.keys():
                 ordered_result[key] = result[key]
             # apply pre_validators
@@ -278,6 +285,7 @@ class ValueComment(formencode.FancyValidator):
         return v
 
 
+
 class SemicolonList(formencode.FancyValidator):
     """Applies a given validator to a semicolon separated list of values and returns a python list.
     For an empty string an empty list is returned."""
@@ -336,23 +344,293 @@ class DateNone(NoneValidator):
         NoneValidator.__init__(self, GenericDateTime('%Y-%m-%d'))
 
 
-class Geo(formencode.FancyValidator):
-    """Formats to coordinates '47.076207 N 11.453553 E' to the (latitude, longitude) tuplet."""
-    def to_python(self, value, state=None):
-        self.assert_string(value, state)
-        r = re.match('(\d+\.\d+) N (\d+\.\d+) E', value)
-        if r is None: raise formencode.Invalid("Coordinates '%s' have not a format like '47.076207 N 11.453553 E'" % value, value, state)
-        return (float(r.groups()[0]), float(r.groups()[1]))
-    
-    def from_python(self, value, state=None):
-        latitude, longitude = value
-        return '%.6f N %.6f E' % (latitude, longitude)
 
+# Meta converter types and functions
+# ----------------------------------
+
+class Converter:
+    @classmethod
+    def from_str(cls, value):
+        return value
+
+    @classmethod
+    def to_str(cls, value):
+        return str(value)
+
+
+FromToConverter = namedtuple('FromToConverter', ['from_str', 'to_str'])
+
+
+def opt_from_str(value, from_str, none=None):
+    return none if value == '' else from_str(value)
+
+
+def opt_to_str(value, to_str, none=None):
+    return '' if value == none else to_str(value)
+
+
+class OptionalConverter(Converter):
+    converter = Converter
+    none = None
+
+    @classmethod
+    def from_str(cls, value):
+        return opt_from_str(value, cls.converter, cls.none)
+
+    @classmethod
+    def to_str(cls, value):
+        return opt_to_str(value, cls.converter, cls.none)
+
+
+def choice_from_str(value, choices):
+    if value not in choices:
+        raise ValueError('{} is an invalid value')
+    return value
+
+
+def dictkey_from_str(value, key_str_dict):
+    try:
+        return dict(list(zip(key_str_dict.values(), key_str_dict.keys())))[value]
+    except KeyError:
+        raise ValueError("Invalid value '{}'".format(value))
+
+
+def dictkey_to_str(value, key_str_dict):
+    try:
+        return key_str_dict[value]
+    except KeyError:
+        raise ValueError("Invalid value '{}'".format(value))
+
+
+class DictKeyConverter(Converter):
+    key_str_dict = OrderedDict()
+
+    @classmethod
+    def from_str(cls, value):
+        return dictkey_from_str(value, cls.key_str_dict)
+
+    @classmethod
+    def to_str(cls, value):
+        return dictkey_to_str(value, cls.key_str_dict)
+
+
+
+# Basic type converter functions
+# ------------------------------
+
+
+def str_from_str(value):
+    return value
+
+
+def str_to_str(value):
+    return value
+
+
+def opt_str_from_str(value):
+    return opt_from_str(value, str_from_str)
+
+
+def opt_str_to_str(value):
+    return opt_to_str(value, str_to_str)
+
+
+opt_str_converter = FromToConverter(opt_str_from_str, opt_str_to_str)
+
+
+def req_str_from_str(value):
+    if value == '':
+        raise ValueError('missing required value')
+    return str_from_str(value)
+
+
+class Str(Converter):
+    pass
+
+
+class OptStr(OptionalConverter):
+    converter = Str
+
+
+def int_from_str(value, min=None, max=None):
+    value = int(value)
+    if min is not None and value < min:
+        raise ValueError('{} must be >= than {}'.format(value, min))
+    if max is not None and value > max:
+        raise ValueError('{} must be <= than {}'.format(value, max))
+    return value
+
+
+def int_to_str(value):
+    return str(value)
+
+
+def opt_int_from_str(value, min=None, max=None):
+    return opt_from_str(value, lambda val: int_from_str(val, min, max))
+
+
+def opt_int_to_str(value):
+    return opt_to_str(value, int_to_str)
+
+
+opt_int_converter = FromToConverter(opt_int_from_str, opt_int_to_str)
+
+
+class Int(Converter):
+    min = None
+    max = None
+
+    @classmethod
+    def from_str(cls, value):
+        return int_from_str(value, cls.min, cls.max)
+
+
+IntConverter = FromToConverter(int_from_str, int_to_str)
+
+
+class OptInt(OptionalConverter):
+    converter = Int
+
+
+class DateTime(Converter):
+    format='%Y-%m-%d %H:%M:%S'
+
+    @classmethod
+    def from_str(cls, value):
+        return datetime.datetime.strptime(value, cls.format)
+
+    @classmethod
+    def to_str(cls, value):
+        return value.strftime(cls.format)
+
+
+# Complex types
+# -------------
+
+def enum_from_str(value, from_str=req_str_from_str, separator=';', min_len=0):
+    """Semicolon separated list of entries with the same "type"."""
+    values = value.split(separator)
+    if len(values) == 1 and values[0] == '':
+        values = []
+    if len(values) < min_len:
+        raise ValueError('at least {} entry/entries have to be in the enumeration'.format(min_len))
+    return list(map(from_str, map(str.strip, values)))
+
+
+def enum_to_str(value, to_str=opt_str_to_str, separator='; '):
+    return separator.join(map(to_str, value))
+
+
+# Specific converter functions
+# ----------------------------
+
+BOOL_GERMAN = OrderedDict([(False, 'Nein'), (True, 'Ja')])
+
+
+def bool_german_from_str(value):
+    return dictkey_from_str(value, BOOL_GERMAN)
+
+
+def bool_german_to_str(value):
+    return dictkey_to_str(value, BOOL_GERMAN)
+
+
+def opt_bool_german_from_str(value):
+    return opt_from_str(value, bool_german_from_str)
+
+
+def opt_bool_german_to_str(value):
+    return opt_to_str(value, bool_german_to_str)
+
+
+opt_bool_german_converter = FromToConverter(opt_bool_german_from_str, opt_bool_german_to_str)
+
+
+TRISTATE_GERMAN = OrderedDict([(0.0, 'Nein'), (0.5, 'Teilweise'), (1.0, 'Ja')])
+
+
+def tristate_german_from_str(value):
+    return dictkey_from_str(value, TRISTATE_GERMAN)
+
+
+def tristate_german_to_str(value):
+    return dictkey_to_str(value, TRISTATE_GERMAN)
+
+
+def opt_tristate_german_from_str(value):
+    return opt_from_str(value, tristate_german_from_str)
+
+
+def opt_tristate_german_to_str(value):
+    return opt_to_str(value, tristate_german_to_str)
+
+
+def meter_from_str(value):
+    return int_from_str(value, min=0)
+
+
+def meter_to_str(value):
+    return int_to_str(value)
+
+
+def opt_meter_from_str(value):
+    return opt_from_str(value, meter_from_str)
+
+
+def opt_meter_to_str(value):
+    return opt_to_str(value, meter_to_str)
+
+
+opt_meter_converter = FromToConverter(opt_meter_from_str, opt_meter_to_str)
+
+
+def minutes_from_str(value):
+    return int_from_str(value, min=0)
+
+
+def minutes_to_str(value):
+    return int_to_str(value)
+
+
+def opt_minutes_from_str(value):
+    return opt_from_str(value, minutes_from_str)
+
+
+def opt_minutes_to_str(value):
+    return opt_to_str(value, minutes_to_str)
+
+
+opt_minutes_converter = FromToConverter(opt_minutes_from_str, opt_minutes_to_str)
+
+
+LonLat = namedtuple('LonLat', ['lon', 'lat'])
+
+
+lonlat_none = LonLat(None, None)
+
+
+def lonlat_from_str(value):
+    """Converts a winterrodeln geo string like '47.076207 N 11.453553 E' (being '<latitude> N <longitude> E'
+    to the LonLat(lon, lat) named  tupel."""
+    r = re.match('(\d+\.\d+) N (\d+\.\d+) E', value)
+    if r is None: raise ValueError("Coordinates '{}' have not a format like '47.076207 N 11.453553 E'".format(value))
+    return LonLat(float(r.groups()[1]), float(r.groups()[0]))
+
+
+def lonlat_to_str(value):
+    return '{:.6f} N {:.6f} E'.format(value.lat, value.lon)
+
+
+def opt_lonlat_from_str(value):
+    return opt_from_str(value, lonlat_from_str, lonlat_none)
+
+
+def opt_lonlat_to_str(value):
+    return opt_to_str(value, lonlat_to_str, lonlat_none)
+
+
+opt_lonlat_converter = FromToConverter(opt_lonlat_from_str, opt_lonlat_to_str)
 
-class GeoNone(NoneValidator):
-    """Formats to coordinates '47.076207 N 11.453553 E' to the (latitude, longitude) tuplet."""
-    def __init__(self):
-        NoneValidator.__init__(self, Geo(), (None, None))
 
 
 class MultiGeo(formencode.FancyValidator):
@@ -449,100 +727,145 @@ class MultiGeo(formencode.FancyValidator):
         return "\n".join(result)
 
 
-# deprecated
-class AustrianPhoneNumber(formencode.FancyValidator):
-    """
-    Validates and converts phone numbers to +##/###/####### or +##/###/#######-### (having an extension)
-    @param  default_cc      country code for prepending if none is provided, defaults to 43 (Austria)
-    ::
-        >>> v = AustrianPhoneNumber()
-        >>> v.to_python(u'0512/12345678')
-        u'+43/512/12345678'
-        >>> v.to_python(u'+43/512/12345678')
-        u'+43/512/12345678'
-        >>> v.to_python(u'0512/1234567-89') # 89 is the extension
-        u'+43/512/1234567-89'
-        >>> v.to_python(u'+43/512/1234567-89')
-        u'+43/512/1234567-89'
-        >>> v.to_python(u'0512 / 12345678') # Exception
-        >>> v.to_python(u'0512-12345678') # Exception
-    """
-    # Inspired by formencode.national.InternationalPhoneNumber
+DIFFICULTY_GERMAN = OrderedDict([(1, 'leicht'), (2, 'mittel'), (3, 'schwer')])
 
-    default_cc = 43 # Default country code
-    messages = {'phoneFormat': "'%%(value)s' is an invalid format. Please enter a number in the form +43/###/####### or 0###/########."}
 
-    def to_python(self, value, state=None):
-        self.assert_string(value, state)
-        m = re.match('^(?:\+(\d+)/)?([\d/]+)(?:-(\d+))?$', value)
-        # This will separate 
-        #     u'+43/512/1234567-89'  => (u'43', u'512/1234567', u'89')
-        #     u'+43/512/1234/567-89' => (u'43', u'512/1234/567', u'89')
-        #     u'+43/512/1234/567'    => (u'43', u'512/1234/567', None)
-        #     u'0512/1234567'        => (None, u'0512/1234567', None)
-        if m is None: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, state)
-        (country, phone, extension) = m.groups()
-        
-        # Phone
-        if phone.find('//') > -1 or phone.count('/') == 0: raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, state)
-        
-        # Country
-        if country is None:
-            if phone[0] != '0': raise formencode.Invalid(self.message('phoneFormat', None) % {'value': value}, value, state)
-            phone = phone[1:]
-            country = str(self.default_cc)
-        
-        if extension is None: return '+%s/%s' % (country, phone)
-        return '+%s/%s-%s' % (country, phone, extension)
+def difficulty_german_from_str(value):
+    return dictkey_from_str(value, DIFFICULTY_GERMAN)
 
 
-# Deprecated
-class AustrianPhoneNumberNone(NoneValidator):
-    def __init__(self):
-        NoneValidator.__init__(self, AustrianPhoneNumber())
+def difficulty_german_to_str(value):
+    return dictkey_to_str(value, DIFFICULTY_GERMAN)
 
 
-# Deprecated
-class AustrianPhoneNumberCommentLoop(NoneValidator):
-    def __init__(self):
-        NoneValidator.__init__(self, Loop(ValueComment(AustrianPhoneNumber())))
+def opt_difficulty_german_from_str(value):
+    return opt_from_str(value, difficulty_german_from_str)
 
 
-class GermanDifficulty(DictValidator):
-    """Converts the difficulty represented in a number from 1 to 3 (or None)
-    to a German representation:
-    u''       <=> None
-    u'leicht' <=> 1
-    u'mittel' <=> 2
-    u'schwer' <=> 3"""
-    def __init__(self):
-        DictValidator.__init__(self, {'': None, 'leicht': 1, 'mittel': 2, 'schwer': 3})
+def opt_difficulty_german_to_str(value):
+    return opt_to_str(value, difficulty_german_to_str)
 
 
-class GermanAvalanches(DictValidator):
-    """Converts the avalanches property represented as number from 1 to 4 (or None)
-    to a German representation:
-    u''             <=> None
-    u'kaum'         <=> 1
-    u'selten'       <=> 2
-    u'gelegentlich' <=> 3
-    u'häufig'       <=> 4"""
-    def __init__(self):
-        DictValidator.__init__(self, {'': None, 'kaum': 1, 'selten': 2, 'gelegentlich': 3, 'häufig': 4})
-
-
-class GermanPublicTransport(DictValidator):
-    """Converts the public_transport property represented as number from 1 to 6 (or None)
-    to a German representation:
-    u''            <=> None
-    u'Sehr gut'    <=> 1
-    u'Gut'         <=> 2
-    u'Mittelmäßig' <=> 3
-    u'Schlecht'    <=> 4
-    u'Nein'        <=> 5
-    u'Ja'          <=> 6"""
-    def __init__(self):
-        DictValidator.__init__(self, {'': None, 'Sehr gut': 1, 'Gut': 2, 'Mittelmäßig': 3, 'Schlecht': 4, 'Nein': 5, 'Ja': 6})
+opt_difficulty_german_converter = FromToConverter(opt_difficulty_german_from_str, opt_difficulty_german_to_str)
+
+
+AVALANCHES_GERMAN = OrderedDict([(1, 'kaum'), (2, 'selten'), (3, 'gelegentlich'), (4, 'häufig')])
+
+
+def avalanches_german_from_str(value):
+    return dictkey_from_str(value, AVALANCHES_GERMAN)
+
+
+def avalanches_german_to_str(value):
+    return dictkey_to_str(value, AVALANCHES_GERMAN)
+
+
+def opt_avalanches_german_from_str(value):
+    return opt_from_str(value, avalanches_german_from_str)
+
+
+def opt_avalanches_german_to_str(value):
+    return opt_to_str(value, avalanches_german_to_str)
+
+
+opt_avalanches_german_converter = FromToConverter(opt_avalanches_german_from_str, opt_avalanches_german_to_str)
+
+
+PUBLIC_TRANSPORT_GERMAN = OrderedDict([(1, 'Sehr gut'), (2, 'Gut'), (3, 'Mittelmäßig'), (4, 'Schlecht'), (5, 'Nein'), (6, 'Ja')])
+
+
+def public_transport_german_from_str(value):
+    return dictkey_from_str(value, PUBLIC_TRANSPORT_GERMAN)
+
+
+def public_transport_german_to_str(value):
+    return dictkey_to_str(value, PUBLIC_TRANSPORT_GERMAN)
+
+
+def opt_public_transport_german_from_str(value):
+    return opt_from_str(value, public_transport_german_from_str)
+
+
+def opt_public_transport_german_to_str(value):
+    return opt_to_str(value, public_transport_german_to_str)
+
+
+opt_public_transport_german_converter = FromToConverter(opt_public_transport_german_from_str, opt_public_transport_german_to_str)
+
+
+def value_comment_from_str(value, value_from_str=str_from_str, comment_from_str=str_from_str, comment_optional=False):
+    """Makes it possible to have a mandatory comment in parenthesis at the end of the string."""
+    open_brackets = 0
+    comment = ''
+    comment_end_pos = None
+    for i, char in enumerate(value[::-1]):
+        if char == ')':
+            open_brackets += 1
+            if open_brackets == 1:
+                comment_end_pos = i
+                if len(value[-1-comment_end_pos:].rstrip()) > 1:
+                    raise ValueError('invalid characters after comment')
+        elif char == '(':
+            open_brackets -= 1
+            if open_brackets == 0:
+                comment = value[-i:-1-comment_end_pos]
+                value = value[:-i-1].rstrip()
+                break
+    else:
+        if open_brackets > 0:
+            raise ValueError('bracket mismatch')
+        if not comment_optional:
+            raise ValueError('mandatory comment not found')
+    return value_from_str(value), comment_from_str(comment)
+
+
+def value_comment_to_str(value, value_to_str=str_to_str, comment_to_str=str_to_str, comment_optional=False):
+    left = value_to_str(value[0])
+    comment = comment_to_str(value[1])
+    if len(comment) > 0 or not comment_optional:
+        comment = '({})'.format(comment)
+    if len(left) == 0:
+        return comment
+    if len(comment) == 0:
+        return left
+    return '{} {}'.format(left, comment)
+
+
+def opt_tristate_german_comment_from_str(value):
+    """Ja, Nein or Vielleicht, optionally with comment in parenthesis."""
+    return value_comment_from_str(value, opt_tristate_german_from_str, opt_str_from_str, True)
+
+
+def opt_tristate_german_comment_to_str(value):
+    return value_comment_to_str(value, opt_tristate_german_to_str, opt_str_to_str, True)
+
+
+opt_tristate_german_comment_converter = FromToConverter(opt_tristate_german_comment_from_str, opt_tristate_german_comment_to_str)
+
+
+def no_german_from_str(value, from_str=req_str_from_str, use_tuple=True, no_value=None):
+    if value == 'Nein':
+        return (False, no_value) if use_tuple else no_value
+    return (True, from_str(value)) if use_tuple else from_str(value)
+
+
+def no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None):
+    if use_tuple:
+        if not value[0]:
+            return 'Nein'
+        return to_str(value[1])
+    else:
+        if value == no_value:
+            return 'Nein'
+        return to_str(value)
+
+
+def opt_no_german_from_str(value, from_str=str_from_str, use_tuple=True, no_value=None, none=(None, None)):
+    return opt_from_str(value, lambda v: no_german_from_str(v, from_str, use_tuple, no_value), none)
+
+
+def opt_no_german_to_str(value, to_str=str_to_str, use_tuple=True, no_value=None, none=(None, None)):
+    return opt_to_str(value, lambda v: no_german_to_str(v, to_str, use_tuple, no_value), none)
 
 
 class GermanTristateFloatComment(ValueComment):
@@ -560,6 +883,39 @@ class GermanTristateFloatComment(ValueComment):
         ValueComment.__init__(self, GermanTristateFloat())
 
 
+def night_light_from_str(value):
+    """'Beleuchtungsanlage' Tristate with optional comment:
+    ''                  <=> (None, None)
+    'Ja'                <=> (1.0,  None)
+    'Teilweise'         <=> (0.5,  None)
+    'Nein'              <=> (0.0,  None)
+    'Ja (aber schmal)'  <=> (1.0,  'aber schmal')
+    'Teilweise (oben)'  <=> (0.5,  'oben')
+    'Nein (aber breit)' <=> (0.0,  'aber breit')
+    """
+    return
+
+
+class NightLightDays(Int):
+    min = 0
+    max = 7
+
+
+class OptNightLightDays(OptionalConverter):
+    converter = NightLightDays
+
+
+def nightlightdays_from_str(value):
+    return value_comment_from_str(value, lambda val: opt_from_str(val, lambda v: int_from_str(v, min=0, max=7)), opt_str_from_str, comment_optional=True)
+
+
+def nightlightdays_to_str(value):
+    return value_comment_to_str(value, lambda val: opt_to_str(val, int_to_str), opt_str_to_str, comment_optional=True)
+
+
+nightlightdays_converter = FromToConverter(nightlightdays_from_str, nightlightdays_to_str)
+
+
 class UnsignedCommentNone(NoneValidator):
     """Converts the a property with unsigned values an optional comment
     in parenthesis to a text:
@@ -572,26 +928,58 @@ class UnsignedCommentNone(NoneValidator):
         NoneValidator.__init__(self, ValueComment(Unsigned(max=max)), (None, None))
 
 
-class GermanCachet(formencode.FancyValidator):
+CACHET_REGEXP = [r'(Tiroler Naturrodelbahn-Gütesiegel) ([12]\d{3}) (leicht|mittel|schwer)$']
+
+
+def single_cachet_german_from_str(value):
+    for pattern in CACHET_REGEXP:
+        match = re.match(pattern, value)
+        if match:
+            return match.groups()
+    raise ValueError("'{}' is no valid cachet".format(value))
+
+
+def single_cachet_german_to_str(value):
+    return ' '.join(value)
+
+
+def cachet_german_from_str(value):
     """Converts a "Gütesiegel":
-    u'' <=> None
-    u'Nein' <=> 'Nein'
-    u'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' <=> u'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'"""
-    def to_python(self, value, state=None):
-        self.assert_string(value, state)
-        if value == '': return None
-        elif value == 'Nein': return value
-        elif value.startswith('Tiroler Naturrodelbahn-Gütesiegel '):
-            p = value.split(" ")
-            Unsigned().to_python(p[2], state) # check if year can be parsed
-            if not p[3] in ['leicht', 'mittel', 'schwer']: raise formencode.Invalid("Unbekannter Schwierigkeitsgrad", value, state)
-            return value
-        else: raise formencode.Invalid("Unbekanntes Gütesiegel", value, state)
+    '' => None
+    'Nein' => []
+    'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel' => [('Tiroler Naturrodelbahn-Gütesiegel', '2009', 'mittel')]"""
+    return opt_no_german_from_str(value, lambda val: enum_from_str(val, single_cachet_german_from_str), False, [], None)
+
     
-    def from_python(self, value, state=None):
-        if value is None: return ''
-        assert value != ''
-        return self.to_python(value, state)
+def cachet_german_to_str(value):
+    return opt_no_german_to_str(value, lambda val: enum_to_str(val, single_cachet_german_to_str), False, [], None)
+
+
+cachet_german_converter = FromToConverter(cachet_german_from_str, cachet_german_to_str)
+
+
+def url_from_str(value):
+    result = urllib.parse.urlparse(value)
+    if result.scheme not in ['http', 'https']:
+        raise ValueError('scheme has to be http or https')
+    if not result.netloc:
+        raise ValueError('url does not contain netloc')
+    return value
+
+
+def url_to_str(value):
+    return value
+
+
+def webauskunft_from_str(value):
+    return opt_no_german_from_str(value, url_from_str)
+
+
+def webauskunft_to_str(value):
+    return opt_no_german_to_str(value, url_to_str)
+
+
+webauskunft_converter = FromToConverter(webauskunft_from_str, webauskunft_to_str)
 
 
 class Url(formencode.FancyValidator):
@@ -631,6 +1019,28 @@ class ValueCommentListNeinLoopNone(NoneValidator):
         NoneValidator.__init__(self, NeinValidator(Loop(ValueCommentList())))
 
 
+def phone_number_from_str(value):
+    match = re.match(r'\+\d+(-\d+)*$', value)
+    if match is None:
+        raise ValueError('invalid format of phone number - use something like +43-699-1234567')
+    return value
+
+
+def phone_number_to_str(value):
+    return value
+
+
+def telefonauskunft_from_str(value):
+    return opt_no_german_from_str(value, lambda val: enum_from_str(val, lambda v: value_comment_from_str(v, phone_number_from_str, req_str_from_str, False)), False, [], None)
+
+
+def telefonauskunft_to_str(value):
+    return opt_no_german_to_str(value, lambda val: enum_to_str(val, lambda v: value_comment_to_str(v, phone_number_to_str, str_to_str)), False, [], None)
+
+
+telefonauskunft_converter = FromToConverter(telefonauskunft_from_str, telefonauskunft_to_str)
+
+
 class PhoneNumber(formencode.FancyValidator):
     """Telefonnumber in international format, e.g. u'+43-699-1234567'"""
     def __init__(self, default_cc=43):
@@ -758,14 +1168,46 @@ class TupleSecondValidator(formencode.FancyValidator):
 
 class BoolUnicodeTupleValidator(NoneValidator):
     """Translates an unparsed string or u'Nein' to a tuple:
-    u''         <=> (None, None)
-    u'Nein'     <=> (False, None)
-    u'any text' <=> (True, u'any text')
+    ''         <=> (None, None)
+    'Nein'     <=> (False, None)
+    'any text' <=> (True, 'any text')
     """
     def __init__(self, validator=UnicodeNone()):
         NoneValidator.__init__(self, NeinValidator(TupleSecondValidator(True, validator), (False, None)), (None, None))
 
 
+LIFT_GERMAN = ['Sessellift', 'Gondel', 'Linienbus', 'Taxi', 'Sonstige']
+
+
+def lift_german_from_str(value):
+    """Checks a lift_details property. It is a value comment property with the following
+    values allowed:
+    'Sessellift'
+    'Gondel'
+    'Linienbus'
+    'Taxi'
+    'Sonstige'
+    Alternatively, the value u'Nein' is allowed.
+    An empty string maps to (None, None).
+
+    Examples:
+    ''                                       <=> None
+    'Nein'                                   <=> []
+    'Sessellift                              <=> [('Sessellift', None)]
+    'Gondel (nur bis zur Hälfte)'            <=> [('Gondel', 'nur bis zur Hälfte')]
+    'Sessellift; Taxi'                       <=> [('Sessellift', None), ('Taxi', None)]
+    'Sessellift (Wochenende); Taxi (6 Euro)' <=> [('Sessellift', 'Wochenende'), ('Taxi', '6 Euro')]
+    """
+    return opt_no_german_from_str(value, lambda value_enum: enum_from_str(value_enum, lambda value_comment: value_comment_from_str(value_comment, lambda v: choice_from_str(v, LIFT_GERMAN), opt_str_from_str, comment_optional=True)), use_tuple=False, no_value=[], none=None)
+
+
+def lift_german_to_str(value):
+    return opt_no_german_to_str(value, lambda value_enum: enum_to_str(value_enum, lambda value_comment: value_comment_to_str(value_comment, str_to_str, opt_str_to_str, comment_optional=True)), use_tuple=False, no_value=[], none=None)
+
+
+lift_german_converter = FromToConverter(lift_german_from_str, lift_german_to_str)
+
+
 class GermanLift(BoolUnicodeTupleValidator):
     """Checks a lift_details property. It is a value comment property with the following
     values allowed:
@@ -789,59 +1231,139 @@ class GermanLift(BoolUnicodeTupleValidator):
         BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList(DictValidator({'Sessellift': 'Sessellift', 'Gondel': 'Gondel', 'Linienbus': 'Linienbus', 'Taxi': 'Taxi', 'Sonstige': 'Sonstige'}))))
         
 
-class SledRental(BoolUnicodeTupleValidator):
-    """The value can be an empty string, u'Nein' or a comma-separated list of unicode strings with optional comments.
-    u''                                       <=> (None, None)
-    u'Nein'                                   <=> (False, None)
-    u'Talstation (nur mit Ticket); Schneealm' <=> (True, u'Talstation (nur mit Ticket); Schneealm')"""
-    def __init__(self):
-        BoolUnicodeTupleValidator.__init__(self, Loop(ValueCommentList()))
+def sledrental_from_str(value):
+    """The value can be an empty string, 'Nein' or a semicolon-separated list of strings with optional comments.
+    ''                                       => None
+    'Nein'                                   => []
+    'Talstation (nur mit Ticket); Schneealm' => [('Talstation', 'nur mit Ticket'), ('Schneealm', None)]"""
+    return opt_no_german_from_str(value, lambda val: enum_from_str(val, lambda v: value_comment_from_str(v, req_str_from_str, opt_str_from_str, True)), False, [], None)
 
 
-class RodelbahnboxDictValidator(OrderedSchema):
-    """Takes the fields of the Rodelbahnbox as dict of strings and returns them as dict of appropriet types."""
-    def __init__(self):
-        self.add_field('Position', GeoNone()) # '47.583333 N 15.75 E'
-        self.add_field('Position oben', GeoNone()) # '47.583333 N 15.75 E'
-        self.add_field('Höhe oben', UnsignedNone()) # '2000'
-        self.add_field('Position unten', GeoNone()) # '47.583333 N 15.75 E'
-        self.add_field('Höhe unten', UnsignedNone()) # '1200'
-        self.add_field('Länge', UnsignedNone()) # 3500
-        self.add_field('Schwierigkeit', GermanDifficulty()) # 'mittel'
-        self.add_field('Lawinen', GermanAvalanches()) # 'kaum'
-        self.add_field('Betreiber', UnicodeNone()) # 'Max Mustermann'
-        self.add_field('Öffentliche Anreise', GermanPublicTransport()) # 'Mittelmäßig'
-        self.add_field('Aufstieg möglich', GermanBoolNone()) # 'Ja'
-        self.add_field('Aufstieg getrennt', GermanTristateFloatComment()) # 'Ja'
-        self.add_field('Gehzeit', UnsignedNone()) # 90
-        self.add_field('Aufstiegshilfe', GermanLift()) # 'Gondel (unterer Teil)'
-        self.add_field('Beleuchtungsanlage', GermanTristateFloatComment())
-        self.add_field('Beleuchtungstage', UnsignedCommentNone(7)) # '3 (Montag, Mittwoch, Freitag)'
-        self.add_field('Rodelverleih', SledRental()) # 'Talstation Serlesbahnan'
-        self.add_field('Gütesiegel', GermanCachet()) # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
-        self.add_field('Webauskunft', UrlNeinNone()) # 'http://www.nösslachhütte.at/page9.php'
-        self.add_field('Telefonauskunft', PhoneCommentListNeinLoopNone(comments_are_optional=False)) # '+43-664-5487520 (Mitterer Alm)'
-        self.add_field('Bild', UnicodeNone())
-        self.add_field('In Übersichtskarte', GermanBoolNone())
-        self.add_field('Forumid', UnsignedNeinNone())
-
-
-class GasthausboxDictValidator(OrderedSchema):
-    """Takes the fields of the Gasthausbox as dict of strings and returns them as dict of appropriet types."""
-    def __init__(self):
-        self.add_field('Position', GeoNone()) # '47.583333 N 15.75 E'
-        self.add_field('Höhe', UnsignedNone())
-        self.add_field('Betreiber', UnicodeNone())
-        self.add_field('Sitzplätze', UnsignedNone())
-        self.add_field('Übernachtung', BoolUnicodeTupleValidator())
-        self.add_field('Rauchfrei', GermanTristateTuple())
-        self.add_field('Rodelverleih', BoolUnicodeTupleValidator())
-        self.add_field('Handyempfang', ValueCommentListNeinLoopNone())
-        self.add_field('Homepage', UrlNeinNone())
-        self.add_field('E-Mail', EmailCommentListNeinLoopNone(allow_masked_email=True))
-        self.add_field('Telefon', PhoneCommentListNeinLoopNone(comments_are_optional=True))
-        self.add_field('Bild', UnicodeNone())
-        self.add_field('Rodelbahnen', WikiPageListLoopNone())
+def sledrental_to_str(value):
+    return opt_no_german_to_str(value, lambda val: enum_to_str(val, lambda v: value_comment_to_str(v, str_to_str, opt_str_to_str, True)), False, [], None)
+
+
+sledrental_converter = FromToConverter(sledrental_from_str, sledrental_to_str)
+
+
+class ValueErrorList(ValueError):
+    pass
+
+
+def box_from_template(template, name, converter_dict):
+    if template.name.strip() != name:
+        raise ValueError('Box name has to be "{}"'.format(name))
+    result = OrderedDict()
+    exceptions_dict = OrderedDict()
+    # check values
+    for key, converter in converter_dict.items():
+        try:
+            if not template.has(key):
+                raise ValueError('Missing parameter "{}"'.format(key))
+            result[key] = converter.from_str(str(template.get(key).value.strip()))
+        except ValueError as e:
+            exceptions_dict[key] = e
+    # check if keys are superfluous
+    superfluous_keys = {str(p.name.strip()) for p in template.params} - set(converter_dict.keys())
+    for key in superfluous_keys:
+        exceptions_dict[key] = ValueError('Superfluous parameter: "{}"'.format(key))
+    if len(exceptions_dict) > 0:
+        raise ValueErrorList('{} error(s) occurred when parsing template parameters.'.format(len(exceptions_dict)), exceptions_dict)
+    return result
+
+
+def box_to_template(value, name, converter_dict):
+    template = mwparserfromhell.nodes.template.Template(name)
+    for key, converter in converter_dict.items():
+        template.add(key, converter.to_str(value[key]))
+    return template
+
+
+def template_from_str(value, name):
+    wikicode = mwparserfromhell.parse(value)
+    template_list = wikicode.filter_templates(name)
+    if len(name) == 0:
+        raise ValueError('No "{}" template was found'.format(name))
+    if len(template_list) > 1:
+        raise ValueError('{} "{}" templates were found'.format(len(template_list), name))
+    return template_list[0]
+
+
+def box_from_str(value, name, converter_dict):
+    template = template_from_str(value, name)
+    return box_from_template(template, name, converter_dict)
+
+
+def box_to_str(value, name, converter_dict):
+    return str(box_to_template(value, name, converter_dict))
+
+
+RODELBAHNBOX_TEMPLATE_NAME = 'Rodelbahnbox'
+
+
+RODELBAHNBOX_DICT = OrderedDict([
+    ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
+    ('Position oben', opt_lonlat_converter), # '47.583333 N 15.75 E'
+    ('Höhe oben', opt_meter_converter), # '2000'
+    ('Position unten', opt_lonlat_converter), # '47.583333 N 15.75 E'
+    ('Höhe unten', opt_meter_converter), # '1200'
+    ('Länge', opt_meter_converter), # 3500
+    ('Schwierigkeit', opt_difficulty_german_converter), # 'mittel'
+    ('Lawinen', opt_avalanches_german_converter), # 'kaum'
+    ('Betreiber', opt_str_converter), # 'Max Mustermann'
+    ('Öffentliche Anreise', opt_public_transport_german_converter), # 'Mittelmäßig'
+    ('Aufstieg möglich', opt_bool_german_converter), # 'Ja'
+    ('Aufstieg getrennt', opt_tristate_german_comment_converter), # 'Ja'
+    ('Gehzeit', opt_minutes_converter), # 90
+    ('Aufstiegshilfe', lift_german_converter), # 'Gondel (unterer Teil)'
+    ('Beleuchtungsanlage', opt_tristate_german_comment_converter),
+    ('Beleuchtungstage', nightlightdays_converter), # '3 (Montag, Mittwoch, Freitag)'
+    ('Rodelverleih', sledrental_converter), # 'Talstation Serlesbahnan'
+    ('Gütesiegel', cachet_german_converter), # 'Tiroler Naturrodelbahn-Gütesiegel 2009 mittel'
+    ('Webauskunft', webauskunft_converter), # 'http://www.nösslachhütte.at/page9.php'
+    ('Telefonauskunft', telefonauskunft_converter), # '+43-664-5487520 (Mitterer Alm)'
+    ('Bild', opt_str_converter),
+    ('In Übersichtskarte', opt_bool_german_converter),
+    ('Forumid', opt_int_converter)
+])
+
+
+def rodelbahnbox_from_template(template):
+    return box_from_template(template, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
+
+
+def rodelbahnbox_to_template(value):
+    return box_to_template(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
+
+
+def rodelbahnbox_from_str(value):
+    return box_from_str(value, RODELBAHNBOX_TEMPLATE_NAME, RODELBAHNBOX_DICT)
+
+
+def rodelbahnbox_to_str(value):
+    template = rodelbahnbox_to_template(value)
+    template_to_table(template, 20)
+    return str(template)
+
+
+GASTHAUSBOX_TEMPLATE_NAME = 'Gasthausbox'
+
+
+GASTHAUSBOX_DICT = OrderedDict([
+    ('Position', opt_lonlat_converter), # '47.583333 N 15.75 E'
+    ('Höhe', opt_meter_converter),
+    ('Betreiber', opt_str_converter),
+    ('Sitzplätze', opt_int_converter),
+    ('Übernachtung', BoolUnicodeTupleValidator()),
+    ('Rauchfrei', opt_tristate_german_validator),
+    ('Rodelverleih', BoolUnicodeTupleValidator()),
+    ('Handyempfang', ValueCommentListNeinLoopNone()),
+    ('Homepage', webauskunft_converter),
+    ('E-Mail', EmailCommentListNeinLoopNone(allow_masked_email=True)),
+    ('Telefon', PhoneCommentListNeinLoopNone(comments_are_optional=True)),
+    ('Bild', opt_str_converter),
+    ('Rodelbahnen', WikiPageListLoopNone())])
+
 
 
 def sledrun_page_title_to_pretty_url(page_title):