The change of the wrreportcache table caused necessary changes in this module.
[philipp/winterrodeln/wrpylib.git] / wrpylib / mwmarkup.py
1 #!/usr/bin/python2.6
2 # -*- coding: iso-8859-15 -*-
3 # $Id$
4 # $HeadURL$
5 """This module contains general functions that help parsing the mediawiki markup.
6 I looked for an already existing MediaWiki parser in Python but I didn't find anything 
7 that convinced me. However, here are the links:
8
9 * py-wikimarkup https://github.com/dcramer/py-wikimarkup
10 * mwlib http://code.pediapress.com/wiki/wiki
11 """
12 import re
13 import xml.etree.ElementTree
14
15
16 def find_template(wikitext, template_title):
17     """Returns the tuple (start, end) of the first occurence of the template '{{template ...}} within wikitext'.
18     (None, None) is returned if the template is not found.
19     If you are sure that the wikitext contains the template, the template could be extracted like follows:
20
21     >>> wikitext = u'This is a {{Color|red|red text}} template.'
22     >>> start, end = find_template(wikitext, u'Color')
23     >>> print wikitext[start:end]
24     {{Color|red|red text}}
25
26     or just:
27
28     >>> print wikitext.__getslice__(*find_template(wikitext, u'Color'))
29     {{Color|red|red text}}
30
31     The search is done with regular expression.
32
33     :param wikitext: The text (preferalbe unicode) that has the template in it.
34     :param template_title: The page title of the template with or without namespace (but as in the wikitext).
35     :return: 
36         (start, end) of the first occurence with start >= 0 and end > start.
37         (None, None) if the template is not found.
38     """ 
39     match = re.search(u"\{\{" + template_title + "[^\}]*\}\}", wikitext,  re.DOTALL)
40     if match is None: return None, None
41     return match.start(), match.end()
42
43
44
45 def split_template(template):
46     """Takes a template, like u'{{Color|red|text=Any text}}' and translates it to a Python tuple
47     (template_title, parameters) where parameters is a Python dictionary {1: u'red', u'text'=u'Any text'}.
48     Anonymous parameters get integer keys (converted to unicode) starting with 1 
49     like in MediaWiki, named parameters are unicode strings.
50     Whitespace is stripped.
51     If an unexpected format is encountered, a ValueError is raised."""
52     if not template.startswith(u'{{'): raise ValueError(u'Template does not start with "{{"')
53     if not template.endswith(u'}}'): raise ValueError(u'Template does not end with "}}"')
54     parts = template[2:-2].split(u'|')
55
56     # template name
57     template_title = parts[0].strip()
58     if len(template_title) == 0: raise ValueError(u'Empty template tilte.')
59     del parts[0]
60
61     # anonymous parameters
62     params = {} # result dictionary
63     param_num = 1
64     while len(parts) > 0:
65         equalsign_pos = parts[0].find(u'=')
66         if equalsign_pos >= 0: break # named parameter
67         params[unicode(param_num)] = parts[0].strip()
68         del parts[0]
69         param_num += 1
70
71     # named or numbered parameters
72     while len(parts) > 0:
73         equalsign_pos = parts[0].find(u'=')
74         if equalsign_pos < 0: raise ValueError(u'Anonymous parameter after named parameter.')
75         key, sep, value = parts[0].partition(u'=')
76         key = key.strip()
77         if len(key) == 0: raise ValueError(u'Empty key.')
78         if params.has_key(key): raise ValueError(u'Duplicate key: "{0}"'.format(key))
79         params[key] = value.strip()
80         del parts[0]
81
82     return template_title, params
83
84
85 def create_template(template_title, anonym_params=[], named_param_keys=[], named_param_values=[], as_table=False, as_table_keylen=None):
86     """Formats a MediaWiki template.
87     :param template_title: Unicode string with the template name
88     :param anonym_params: list with parameters without keys
89     :param named_param_keys: list with keys of named parameters
90     :param named_param_values: list with values of named parameters, corresponding to named_param_keys.
91     :param as_table: formats the returned template in one row for each parameter
92     :param as_table_keylen: length of the key field. None for "automatic".
93     :return: unicode template"""
94     pipe_char, equal_char, end_char = (u'\n| ', u' = ', u'\n}}') if as_table else (u'|', u'=', u'}}')
95     parts = [u"{{" + template_title]
96     parts += anonym_params
97     if as_table and as_table_keylen is None:
98         as_table_keylen = max([len(k) for k in named_param_keys])
99     for i in xrange(len(named_param_keys)):
100         key = named_param_keys[i]
101         if as_table: key = key.ljust(as_table_keylen)
102         parts.append(key + equal_char + named_param_values[i])
103     return pipe_char.join(parts) + end_char
104
105
106 def parse_googlemap(wikitext):
107     """Parses the (unicode) u'<googlemap ...>content</googlemap>' of the googlemap extension
108     out of a page. If wikitext does not contain the googlemaps extension text None is returned.
109     If the googlemap contains invalid formatted lines, a RuntimeError is raised.
110
111     :param wikitext: wikitext containing the template. Example:
112
113     wikitext = '''
114     <googlemap version="0.9" lat="47.113291" lon="11.272337" zoom="15">
115     (Parkplatz)47.114958,11.266026
116     Parkplatz
117     
118     (Gasthaus) 47.114715, 11.266262, Alt Bärnbad (Gasthaus)
119     6#FF014E9A
120     47.114715,11.266262
121     47.114135,11.268381
122     47.113421,11.269322
123     47.11277,11.269979
124     47.112408,11.271119
125     </googlemap>
126     '''
127     :returns: the tuple (center, zoom, coords, paths).
128         center is the tuple (lon, lat) of the google maps or (None, None) if not provided
129         zoom is the google zoom level as integer or None if not provided
130         coords is a list of (lon, lat, symbol, title) tuples.
131         paths is a list of (style, coords) tuples.
132         coords is again a list of (lot, lat, symbol, title) tuples."""
133
134     def is_coord(line):
135         """Returns True if the line contains a coordinate."""
136         match = re.search('[0-9]{1,2}\.[0-9]+, ?[0-9]{1,2}\.[0-9]+', line)
137         return not match is None
138
139     def is_path(line):
140         """Returns True if the line contains a path style definition."""
141         match = re.match('[0-9]#[0-9a-fA-F]{8}', line)
142         return not match is None
143
144     def parse_coord(line):
145         """Returns (lon, lat, symbol, title). If symbol or text is not present, None is returned."""
146         match = re.match(u'\(([^)]+)\) ?([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+),(.*)', line)
147         if not match is None: return (float(match.group(3)), float(match.group(2)), match.group(1), match.group(4))
148         match = re.match(u'\(([^)]+)\) ?([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+)', line)
149         if not match is None: return (float(match.group(3)), float(match.group(2)), match.group(1), None)
150         match = re.match(u'([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+),(.*)', line)
151         if not match is None: return (float(match.group(2)), float(match.group(1)), None, match.group(3))
152         match = re.match(u'([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+)', line)
153         if not match is None: return (float(match.group(2)), float(match.group(1)), None, None)
154         return RuntimeError(u'Could not parse line ' + line)
155
156     regexp = re.compile(u"(<googlemap[^>]*>)(.*)(</googlemap>)", re.DOTALL)
157     match = regexp.search(wikitext)
158     if match is None: return None
159     content = match.group(2)
160     gm = xml.etree.ElementTree.XML((match.group(1)+match.group(3)).encode('UTF8'))
161     zoom = gm.get('zoom')
162     lon = gm.get('lon')
163     lat = gm.get('lat')
164     if not zoom is None: zoom = int(zoom)
165     if not lon is None: lon = float(lon)
166     if not lat is None: lat = float(lat)
167     center = (lon, lat)
168
169     coords = []
170     paths = []
171     lines = content.split("\n")
172     i = 0
173     while i < len(lines):
174         line = lines[i].strip()
175         i += 1
176
177         # Skip whitespace
178         if len(line) == 0: continue
179
180         # Handle a path
181         if is_path(line):
182             match = re.match(u'([0-9]#[0-9a-fA-F]{8})', line)
183             style =  match.group(1)
184             local_coords = []
185             while i < len(lines):
186                 line = lines[i].strip()
187                 i += 1
188                 if is_path(line):
189                     i -= 1
190                     break
191                 if is_coord(line):
192                     lon, lat, symbol, title = parse_coord(line)
193                     local_coords.append((lon, lat, symbol, title))
194             paths.append((style, local_coords))
195             continue
196
197         # Handle a coordinate
198         if is_coord(line):
199             lon, lat, symbol, title = parse_coord(line)
200             while i < len(lines):
201                 line = lines[i].strip()
202                 i += 1
203                 if is_path(line) or is_coord(line):
204                     i -= 1
205                     break
206                 if len(line) > 0 and title is None: title = line
207             coords.append((lon, lat, symbol, title))
208             continue
209
210         raise RuntimeError(u'Unknown line syntax: ' + line)
211     return (center, zoom, coords, paths)
212