Updated desciption of function parse_wrmap.
[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 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. It gives wrong results when parsing a template
32     containing the characters "}}"
33
34     :param wikitext: The text (preferalbe unicode) that has the template in it.
35     :param template_title: The page title of the template with or without namespace (but as in the wikitext).
36     :return: 
37         (start, end) of the first occurence with start >= 0 and end > start.
38         (None, None) if the template is not found.
39     """ 
40     match = re.search(u"\{\{" + template_title + "\s*(\|[^\}]*)?\}\}", wikitext,  re.DOTALL)
41     if match is None: return None, None
42     return match.start(), match.end()
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 {u'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: 
102             key = key.ljust(as_table_keylen)
103             parts.append((key + equal_char + named_param_values[i]).rstrip())
104         else:
105             parts.append(key + equal_char + named_param_values[i])
106     return pipe_char.join(parts) + end_char
107
108
109 def find_tag(wikitext, tagname, pos=0):
110     """Returns the tuple (start, end) of the first occurence of the tag '<tag ...>...</tag>'
111     or '<tag ... />'.
112     (None, None) is returned if the tag is not found.
113     If you are sure that the wikitext contains the tag, the tag could be extracted like follows:
114
115     >>> wikitext = u'This is a <tag>mytag</tag> tag.'
116     >>> start, end = find_template(wikitext, u'tag')
117     >>> print wikitext[start:end]
118     <tag>mytag</tag>
119
120     :param wikitext: The text (preferalbe unicode) that has the template in it.
121     :param tagname: Name of the tag, e.g. u'tag' for <tag>.
122     :param pos: position within wikitext to start searching the tag.
123     :return:
124         (start, content, endtag, end). start is the position of '<' of the tag,
125         content is the beginning of the content (after '>'), enttag is the
126         beginning of the end tag ('</') and end is one position after the end tag.
127         For single tags, (start, None, None, end) is returned.
128         If the tag is not found (or only the start tag is present,
129         (None, None, None, None) is returned.
130     """
131     # Find start tag
132     regexp_starttag = re.compile(u"<{0}.*?(/?)>".format(tagname), re.DOTALL)
133     match_starttag = regexp_starttag.search(wikitext, pos)
134     if match_starttag is None:
135         return None, None, None, None
136
137     # does the tag have content?
138     if len(match_starttag.group(1)) == 1: # group(1) is either '' or '/'.
139         # single tag
140         return match_starttag.start(), None, None, match_starttag.end()
141
142     # tag with content
143     regexp_endtag = re.compile(u'</{0}>'.format(tagname), re.DOTALL)
144     match_endtag = regexp_endtag.search(wikitext, match_starttag.end())
145     if match_endtag is None:
146         # No closing tag - error in wikitext
147         return None, None, None, None
148     return match_starttag.start(), match_starttag.end(), match_endtag.start(), match_endtag.end()
149
150
151 def parse_googlemap(wikitext, detail=False):
152     """Parses the (unicode) u'<googlemap ...>content</googlemap>' of the googlemap extension
153     out of a page. If wikitext does not contain the googlemap extension text None is returned.
154     If the googlemap contains invalid formatted lines, a RuntimeError is raised.
155
156     :param wikitext: wikitext containing the template. Example:
157     :param detail: bool. If True, start and end position of <googlemap>...</googlemap> is
158         returned additionally.
159
160     wikitext = '''
161     <googlemap version="0.9" lat="47.113291" lon="11.272337" zoom="15">
162     (Parkplatz)47.114958,11.266026
163     Parkplatz
164     
165     (Gasthaus) 47.114715, 11.266262, Alt Bärnbad (Gasthaus)
166     6#FF014E9A
167     47.114715,11.266262
168     47.114135,11.268381
169     47.113421,11.269322
170     47.11277,11.269979
171     47.112408,11.271119
172     </googlemap>
173     '''
174     :returns: the tuple (center, zoom, coords, paths).
175         center is the tuple (lon, lat) of the google maps or (None, None) if not provided
176         zoom is the google zoom level as integer or None if not provided
177         coords is a list of (lon, lat, symbol, title) tuples.
178         paths is a list of (style, coords) tuples.
179         coords is again a list of (lot, lat, symbol, title) tuples.
180         If detail is True, (center, zoom, coords, paths, start, end) is returned."""
181
182     def is_coord(line):
183         """Returns True if the line contains a coordinate."""
184         match = re.search('[0-9]{1,2}\.[0-9]+, ?[0-9]{1,2}\.[0-9]+', line)
185         return not match is None
186
187     def is_path(line):
188         """Returns True if the line contains a path style definition."""
189         match = re.match('[0-9]#[0-9a-fA-F]{8}', line)
190         return not match is None
191
192     def parse_coord(line):
193         """Returns (lon, lat, symbol, title). If symbol or text is not present, None is returned."""
194         match = re.match(u'\(([^)]+)\) ?([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+), ?(.*)', line)
195         if not match is None: return (float(match.group(3)), float(match.group(2)), match.group(1), match.group(4))
196         match = re.match(u'\(([^)]+)\) ?([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+)', line)
197         if not match is None: return (float(match.group(3)), float(match.group(2)), match.group(1), None)
198         match = re.match(u'([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+), ?(.*)', line)
199         if not match is None: return (float(match.group(2)), float(match.group(1)), None, match.group(3))
200         match = re.match(u'([0-9]{1,2}\.[0-9]+), ?([0-9]{1,2}\.[0-9]+)', line)
201         if not match is None: return (float(match.group(2)), float(match.group(1)), None, None)
202         return RuntimeError(u'Could not parse line ' + line)
203
204     start, content, endtag, end = find_tag(wikitext, 'googlemap')
205     if content is None:
206         return None
207     gm = xml.etree.ElementTree.XML((wikitext[start:content]+wikitext[endtag:end]).encode('UTF8'))
208     zoom = gm.get('zoom')
209     lon = gm.get('lon')
210     lat = gm.get('lat')
211     if not zoom is None: zoom = int(zoom)
212     if not lon is None: lon = float(lon)
213     if not lat is None: lat = float(lat)
214     center = (lon, lat)
215
216     coords = []
217     paths = []
218     lines = wikitext[content:endtag].split("\n")
219     i = 0
220     while i < len(lines):
221         line = lines[i].strip()
222         i += 1
223
224         # Skip whitespace
225         if len(line) == 0: continue
226
227         # Handle a path
228         if is_path(line):
229             match = re.match(u'([0-9]#[0-9a-fA-F]{8})', line)
230             style =  match.group(1)
231             local_coords = []
232             while i < len(lines):
233                 line = lines[i].strip()
234                 i += 1
235                 if is_path(line):
236                     i -= 1
237                     break
238                 if is_coord(line):
239                     lon, lat, symbol, title = parse_coord(line)
240                     local_coords.append((lon, lat, symbol, title))
241             paths.append((style, local_coords))
242             continue
243
244         # Handle a coordinate
245         if is_coord(line):
246             lon, lat, symbol, title = parse_coord(line)
247             while i < len(lines):
248                 line = lines[i].strip()
249                 i += 1
250                 if is_path(line) or is_coord(line):
251                     i -= 1
252                     break
253                 if len(line) > 0 and title is None: title = line
254             coords.append((lon, lat, symbol, title))
255             continue
256
257         raise RuntimeError(u'Unknown line syntax: ' + line)
258     if detail:
259         return (center, zoom, coords, paths, start, end)
260     return (center, zoom, coords, paths)
261