Updated function update_wrmapcache to work with <wrmap> instead of <googlemap>.
[philipp/winterrodeln/wrpylib.git] / wrpylib / mwmarkup.py
1 #!/usr/bin/python2.7
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 class ParseError(RuntimeError):
17     """Exception used by some of the functions"""
18     pass
19
20
21 def find_template(wikitext, template_title):
22     """Returns the tuple (start, end) of the first occurence of the template '{{template ...}} within wikitext'.
23     (None, None) is returned if the template is not found.
24     If you are sure that the wikitext contains the template, the template could be extracted like follows:
25
26     >>> wikitext = u'This is a {{Color|red|red text}} template.'
27     >>> start, end = find_template(wikitext, u'Color')
28     >>> print wikitext[start:end]
29     {{Color|red|red text}}
30
31     or just:
32
33     >>> print wikitext.__getslice__(*find_template(wikitext, u'Color'))
34     {{Color|red|red text}}
35
36     The search is done with regular expression. It gives wrong results when parsing a template
37     containing the characters "}}"
38
39     :param wikitext: The text (preferalbe unicode) that has the template in it.
40     :param template_title: The page title of the template with or without namespace (but as in the wikitext).
41     :return: 
42         (start, end) of the first occurence with start >= 0 and end > start.
43         (None, None) if the template is not found.
44     """ 
45     match = re.search(u"\{\{" + template_title + "\s*(\|[^\}]*)?\}\}", wikitext,  re.DOTALL)
46     if match is None: return None, None
47     return match.start(), match.end()
48
49
50 def split_template(template):
51     """Takes a template, like u'{{Color|red|text=Any text}}' and translates it to a Python tuple
52     (template_title, parameters) where parameters is a Python dictionary {u'1': u'red', u'text'=u'Any text'}.
53     Anonymous parameters get integer keys (converted to unicode) starting with 1 
54     like in MediaWiki, named parameters are unicode strings.
55     Whitespace is stripped.
56     If an unexpected format is encountered, a ValueError is raised."""
57     if not template.startswith(u'{{'): raise ValueError(u'Template does not start with "{{"')
58     if not template.endswith(u'}}'): raise ValueError(u'Template does not end with "}}"')
59     parts = template[2:-2].split(u'|')
60
61     # template name
62     template_title = parts[0].strip()
63     if len(template_title) == 0: raise ValueError(u'Empty template tilte.')
64     del parts[0]
65
66     # anonymous parameters
67     params = {} # result dictionary
68     param_num = 1
69     while len(parts) > 0:
70         equalsign_pos = parts[0].find(u'=')
71         if equalsign_pos >= 0: break # named parameter
72         params[unicode(param_num)] = parts[0].strip()
73         del parts[0]
74         param_num += 1
75
76     # named or numbered parameters
77     while len(parts) > 0:
78         equalsign_pos = parts[0].find(u'=')
79         if equalsign_pos < 0: raise ValueError(u'Anonymous parameter after named parameter.')
80         key, sep, value = parts[0].partition(u'=')
81         key = key.strip()
82         if len(key) == 0: raise ValueError(u'Empty key.')
83         if params.has_key(key): raise ValueError(u'Duplicate key: "{0}"'.format(key))
84         params[key] = value.strip()
85         del parts[0]
86
87     return template_title, params
88
89
90 def create_template(template_title, anonym_params=[], named_param_keys=[], named_param_values=[], as_table=False, as_table_keylen=None):
91     """Formats a MediaWiki template.
92     :param template_title: Unicode string with the template name
93     :param anonym_params: list with parameters without keys
94     :param named_param_keys: list with keys of named parameters
95     :param named_param_values: list with values of named parameters, corresponding to named_param_keys.
96     :param as_table: formats the returned template in one row for each parameter
97     :param as_table_keylen: length of the key field. None for "automatic".
98     :return: unicode template"""
99     pipe_char, equal_char, end_char = (u'\n| ', u' = ', u'\n}}') if as_table else (u'|', u'=', u'}}')
100     parts = [u"{{" + template_title]
101     parts += anonym_params
102     if as_table and as_table_keylen is None:
103         as_table_keylen = max([len(k) for k in named_param_keys])
104     for i in xrange(len(named_param_keys)):
105         key = named_param_keys[i]
106         if as_table: 
107             key = key.ljust(as_table_keylen)
108             parts.append((key + equal_char + named_param_values[i]).rstrip())
109         else:
110             parts.append(key + equal_char + named_param_values[i])
111     return pipe_char.join(parts) + end_char
112
113
114 def find_tag(wikitext, tagname, pos=0):
115     """Returns position information of the first occurence of the tag '<tag ...>...</tag>'
116     or '<tag ... />'.
117     If you are sure that the wikitext contains the tag, the tag could be extracted like follows:
118
119     >>> wikitext = u'This is a <tag>mytag</tag> tag.'
120     >>> start, content, endtag, end = find_template(wikitext, u'tag')
121     >>> print wikitext[start:end]
122     <tag>mytag</tag>
123
124     :param wikitext: The text (preferalbe unicode) that has the template in it.
125     :param tagname: Name of the tag, e.g. u'tag' for <tag>.
126     :param pos: position within wikitext to start searching the tag.
127     :return:
128         (start, content, endtag, end). start is the position of '<' of the tag,
129         content is the beginning of the content (after '>'), enttag is the
130         beginning of the end tag ('</') and end is one position after the end tag.
131         For single tags, (start, None, None, end) is returned.
132         If the tag is not found (or only the start tag is present,
133         (None, None, None, None) is returned.
134     """
135     # Find start tag
136     regexp_starttag = re.compile(u"<{0}.*?(/?)>".format(tagname), re.DOTALL)
137     match_starttag = regexp_starttag.search(wikitext, pos)
138     if match_starttag is None:
139         return None, None, None, None
140
141     # does the tag have content?
142     if len(match_starttag.group(1)) == 1: # group(1) is either '' or '/'.
143         # single tag
144         return match_starttag.start(), None, None, match_starttag.end()
145
146     # tag with content
147     regexp_endtag = re.compile(u'</{0}>'.format(tagname), re.DOTALL)
148     match_endtag = regexp_endtag.search(wikitext, match_starttag.end())
149     if match_endtag is None:
150         # No closing tag - error in wikitext
151         return None, None, None, None
152     return match_starttag.start(), match_starttag.end(), match_endtag.start(), match_endtag.end()
153
154
155 def parse_googlemap(wikitext):
156     """Parses the (unicode) u'<googlemap ...>content</googlemap>' of the googlemap extension.
157     If wikitext does not contain the <googlemap> tag or if the <googlemap> tag contains
158     invalid formatted lines, a ParseError is raised.
159     Use find_tag(wikitext, 'googlemap') to find the googlemap tag within an arbitrary
160     wikitext before using this function.
161
162     :param wikitext: wikitext containing the template. Example:
163
164     wikitext = '''
165     <googlemap version="0.9" lat="47.113291" lon="11.272337" zoom="15">
166     (Parkplatz)47.114958,11.266026
167     Parkplatz
168     
169     (Gasthaus) 47.114715, 11.266262, Alt Bärnbad (Gasthaus)
170     6#FF014E9A
171     47.114715,11.266262
172     47.114135,11.268381
173     47.113421,11.269322
174     47.11277,11.269979
175     47.112408,11.271119
176     </googlemap>
177     '''
178     :returns: the tuple (center, zoom, coords, paths).
179         center is the tuple (lon, lat) of the google maps or (None, None) if not provided
180         zoom is the google zoom level as integer or None if not provided
181         coords is a list of (lon, lat, symbol, title) tuples.
182         paths is a list of (style, coords) tuples.
183         coords is again a list of (lot, lat, symbol, title) tuples."""
184
185     def is_coord(line):
186         """Returns True if the line contains a coordinate."""
187         match = re.search('[0-9]{1,2}\.[0-9]+, ?[0-9]{1,2}\.[0-9]+', line)
188         return not match is None
189
190     def is_path(line):
191         """Returns True if the line contains a path style definition."""
192         match = re.match('[0-9]#[0-9a-fA-F]{8}', line)
193         return not match is None
194
195     def parse_coord(line):
196         """Returns (lon, lat, symbol, title). If symbol or text is not present, None is returned."""
197         match = re.match(u'\(([^)]+)\) ?([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+), ?(.*)', line)
198         if not match is None: return (float(match.group(3)), float(match.group(2)), match.group(1), match.group(4))
199         match = re.match(u'\(([^)]+)\) ?([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+)', line)
200         if not match is None: return (float(match.group(3)), float(match.group(2)), match.group(1), None)
201         match = re.match(u'([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+), ?(.*)', line)
202         if not match is None: return (float(match.group(2)), float(match.group(1)), None, match.group(3))
203         match = re.match(u'([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+)', line)
204         if not match is None: return (float(match.group(2)), float(match.group(1)), None, None)
205         return ParseError(u'Could not parse line ' + line)
206
207     start, content, endtag, end = find_tag(wikitext, 'googlemap')
208     if start is None:
209         raise ParseError('<googlemap> tag not found.')
210     if content is None:
211         xml_only = wikitext[start:endtag]
212     else:
213         xml_only = wikitext[start:content]+wikitext[endtag:end]
214         
215     try:
216         gm = xml.etree.ElementTree.XML(xml_only.encode('UTF8'))
217     except xml.etree.ElementTree.ParseError as e:
218         row, column = e.position
219         raise ParseError("XML parse error in <googlemap ...>.")
220     zoom = gm.get('zoom')
221     lon = gm.get('lon')
222     lat = gm.get('lat')
223     if not zoom is None: zoom = int(zoom)
224     if not lon is None: lon = float(lon)
225     if not lat is None: lat = float(lat)
226     center = (lon, lat)
227
228     coords = []
229     paths = []
230     lines = wikitext[content:endtag].split("\n")
231     i = 0
232     while i < len(lines):
233         line = lines[i].strip()
234         i += 1
235
236         # Skip whitespace
237         if len(line) == 0: continue
238
239         # Handle a path
240         if is_path(line):
241             match = re.match(u'([0-9]#[0-9a-fA-F]{8})', line)
242             style =  match.group(1)
243             local_coords = []
244             while i < len(lines):
245                 line = lines[i].strip()
246                 i += 1
247                 if is_path(line):
248                     i -= 1
249                     break
250                 if is_coord(line):
251                     lon, lat, symbol, title = parse_coord(line)
252                     local_coords.append((lon, lat, symbol, title))
253             paths.append((style, local_coords))
254             continue
255
256         # Handle a coordinate
257         if is_coord(line):
258             lon, lat, symbol, title = parse_coord(line)
259             while i < len(lines):
260                 line = lines[i].strip()
261                 i += 1
262                 if is_path(line) or is_coord(line):
263                     i -= 1
264                     break
265                 if len(line) > 0 and title is None: title = line
266             coords.append((lon, lat, symbol, title))
267             continue
268
269         raise ParseError(u'Unknown line syntax: ' + line)
270     return (center, zoom, coords, paths)
271