1#!/usr/local/bin/python3.8
2#
3# This file is part of the LibreOffice project.
4#
5# This Source Code Form is subject to the terms of the Mozilla Public
6# License, v. 2.0. If a copy of the MPL was not distributed with this
7# file, You can obtain one at http://mozilla.org/MPL/2.0/.
8#
9
10import os, sys, thread, threading, time, re, copy
11import xml.parsers.expat
12import codecs
13from threading import Thread
14
15root="source/"
16max_threads = 25
17
18titles = []
19
20# map of id -> localized text
21localization_data = {}
22
23# to collect a list of pages that will be redirections to the pages with nice
24# names
25redirects = []
26
27# to collect images that we will up-load later
28images = set()
29
30# various types of paragraphs
31replace_paragraph_role = \
32    {'start':{'bascode': '',
33              'code': '<code>',
34              'codeintip': '<code>',
35              'emph' : '', # must be empty to be able to strip empty <emph/>
36              'example': '<code>',
37              'heading1': '= ',
38              'heading2': '== ',
39              'heading3': '=== ',
40              'heading4': '==== ',
41              'heading5': '===== ',
42              'heading6': '====== ',
43              'head1': '= ', # used only in one file, probably in error?
44              'head2': '== ', # used only in one file, probably in error?
45              'listitem': '',
46              'logocode': '<code>',
47              'note': '{{Note|1=',
48              'null': '', # special paragraph for Variable, CaseInline, etc.
49              'ol_item': '',
50              'paragraph': '',
51              'related': '', # used only in one file, probably in error?
52              'relatedtopics': '', # used only in one file, probably in error?
53              'sup' : '',
54              'tablecontent': '| | ',
55              'tablecontentcode': '| | <code>',
56              'tablecontentnote': '| |{{Note|1=',
57              'tablecontenttip': '| |{{Tip|1=',
58              'tablecontentwarning': '| |{{Warning|1=',
59              'tablehead': '! scope="col" | ',
60              'tablenextnote': '\n{{Note|1=',
61              'tablenextpara': '\n',
62              'tablenextparacode': '\n<code>',
63              'tablenexttip': '\n{{Tip|1=',
64              'tablenextwarning': '\n{{Warning|1=',
65              'tip': '{{Tip|1=',
66              'ul_item': '',
67              'variable': '',
68              'warning': '{{Warning|1=',
69             },
70     'end':{'bascode': '\n',
71            'code': '</code>\n\n',
72            'codeintip': '</code>\n\n',
73            'emph' : '',
74            'example': '</code>\n\n',
75            'heading1': ' =\n\n',
76            'heading2': ' ==\n\n',
77            'heading3': ' ===\n\n',
78            'heading4': ' ====\n\n',
79            'heading5': ' =====\n\n',
80            'heading6': ' ======\n\n',
81            'head1': ' =\n\n', # used only in one file, probably in error?
82            'head2': ' ==\n\n', # used only in one file, probably in error?
83            'listitem': '',
84            'logocode': '</code>\n\n',
85            'note': '}}\n\n',
86            'null': '', # special paragraph for Variable, CaseInline, etc.
87            'ol_item': '',
88            'paragraph': '\n\n',
89            'related': '\n\n', # used only in one file, probably in error?
90            'relatedtopics': '\n\n', # used only in one file, probably in error?
91            'sup' : '',
92            'tablecontent': '\n',
93            'tablecontentcode': '</code>\n',
94            'tablecontentnote': '}}\n\n',
95            'tablecontenttip': '}}\n\n',
96            'tablecontentwarning': '}}\n\n',
97            'tablehead': '\n',
98            'tablenextnote': '}}\n\n',
99            'tablenextpara': '\n',
100            'tablenextparacode': '</code>\n',
101            'tablenexttip': '}}\n\n',
102            'tablenextwarning': '}}\n\n',
103            'tip': '}}\n\n',
104            'ul_item': '',
105            'variable': '',
106            'warning': '}}\n\n',
107           },
108     'templ':{'bascode': False,
109              'code': False,
110              'codeintip': False,
111              'emph' : False,
112              'example': False,
113              'heading1': False,
114              'heading2': False,
115              'heading3': False,
116              'heading4': False,
117              'heading5': False,
118              'heading6': False,
119              'head1': False,
120              'head2': False,
121              'listitem': False,
122              'logocode': False,
123              'note': True,
124              'null': False,
125              'ol_item': False,
126              'paragraph': False,
127              'related': False,
128              'relatedtopics': False,
129              'sup' : False,
130              'tablecontent': False,
131              'tablecontentcode': False,
132              'tablecontentnote': True,
133              'tablecontenttip': True,
134              'tablecontentwarning': True,
135              'tablehead': False,
136              'tablenextnote': True,
137              'tablenextpara': False,
138              'tablenextparacode': False,
139              'tablenexttip': True,
140              'tablenextwarning': True,
141              'tip': True,
142              'ul_item': False,
143              'variable': False,
144              'warning': True,
145           }
146    }
147
148section_id_mapping = \
149    {'relatedtopics': 'RelatedTopics'}
150
151# text snippets that we need to convert
152replace_text_list = \
153    [["$[officename]", "{{ProductName}}"],
154     ["%PRODUCTNAME", "{{ProductName}}"],
155     ["$PRODUCTNAME", "{{ProductName}}"],
156     ["font size", u"\u200dfont size"],
157     ["''","<nowiki>''</nowiki>"]
158    ]
159
160def get_link_filename(link, name):
161    text = link.strip()
162    fragment = ''
163    if text.find('http') == 0:
164        text = name
165    else:
166        f = text.find('#')
167        if f >= 0:
168            fragment = text[f:]
169            text = text[0:f]
170
171    for title in titles:
172        try:
173            if title[0].find(text) >= 0:
174                return (title[1].strip(), fragment)
175        except:
176            pass
177    return (link, '')
178
179def replace_text(text):
180    for i in replace_text_list:
181        if text.find(i[0]) >= 0:
182            text = text.replace(i[0],i[1])
183    return text
184
185# modify the text so that in templates like {{Name|something}}, the 'something'
186# does not look like template params
187def escape_equals_sign(text):
188    depth = 0
189    t = ''
190    for i in text:
191        if i == '=':
192            if depth == 0:
193                t = t + '&#61;'
194            else:
195                t = t + '='
196        else:
197            t = t + i
198            if i == '{' or i == '[' or i == '<':
199                depth = depth + 1
200            elif i == '}' or i == ']' or i == '>':
201                depth = depth - 1
202                if depth < 0:
203                    depth = 0
204
205    return t
206
207def xopen(path, mode, encoding):
208    """Wrapper around open() to support both python2 and python3."""
209    if sys.version_info >= (3,):
210        return open(path, mode, encoding=encoding)
211    else:
212        return open(path, mode)
213
214# used by escape_help_text
215helptagre = re.compile('''<[/]??[a-z_\-]+?(?:| +[a-zA-Z]+?=[\\\\]??".*?") *[/]??>''')
216
217def escape_help_text(text):
218    """Escapes the help text as it would be in an SDF file."""
219
220    for tag in helptagre.findall(text):
221        escapethistag = False
222        for escape_tag in ["ahelp", "link", "item", "emph", "defaultinline", "switchinline", "caseinline", "variable", "bookmark_value", "image", "embedvar", "alt"]:
223            if tag.startswith("<%s" % escape_tag) or tag == "</%s>" % escape_tag:
224                escapethistag = True
225        if tag in ["<br/>", "<help-id-missing/>"]:
226            escapethistag = True
227        if escapethistag:
228            escaped_tag = ("\\<" + tag[1:-1] + "\\>")
229            text = text.replace(tag, escaped_tag)
230    return text
231
232
233def load_localization_data(po_root):
234    global localization_data
235    localization_data = {}
236    for root, dirs, files in os.walk(po_root):
237        for file in files:
238            if re.search(r'\.po$', file) == None:
239                continue
240            path = "%s/%s" % (root, file)
241            sock = xopen(path, "r", encoding='utf-8')
242            hashKey = None
243            transCollecting = False
244            trans = ""
245            it = iter(sock)
246            line = next(it, None)
247            while line != None:
248                line=line.decode("utf-8")
249                if line.startswith('msgctxt ""'): # constructing the hashKey
250                    key=[]
251                    allGood = True
252                    i=0
253                    while i<2 and allGood:
254                        msgctxt_line = next(it, None);
255                        if  msgctxt_line != None and msgctxt_line.strip().startswith('"'):
256                            key.append( msgctxt_line[1:-4] ) #-4 cuts \\n"\n from the end of the line
257                            i=i+1
258                        else:
259                            allGood = False
260                    if i==2: #hash key is allowed to be constructed
261                        hashKey = '#'.join( (re.sub(r'^.*helpcontent2/source/', r'source/', path[:-3]) + '/' + key[0] , key[1]) )
262                    else:
263                        hashKey = None
264                elif hashKey != None: # constructing trans value for hashKey
265                    if transCollecting:
266                        if line.startswith('"'):
267                            trans= trans + line.strip()[1:-1]
268                        else:
269                            transCollecting = False
270                            localization_data[hashKey] = escape_help_text(trans)
271                            hashKey = None
272                    elif line.startswith('msgstr '):
273                        trans = line.strip()[8:-1]
274                        if trans == '': # possibly multiline
275                            transCollecting = True
276                        else:
277                            localization_data[hashKey] = escape_help_text(trans)
278                            hashKey = None
279                line = next(it, None)
280    return True
281
282def unescape(str):
283    unescape_map = {'<': {True:'<', False:'&lt;'},
284                    '>': {True:'>', False:'&gt;'},
285                    '&': {True:'&', False:'&amp;'},
286                    '"': {True:'"', False:'"'}}
287    result = ''
288    escape = False
289    for c in str:
290        if c == '\\':
291            if escape:
292                result = result + '\\'
293                escape = False
294            else:
295                escape = True
296        else:
297            try:
298                replace = unescape_map[c]
299                result = result + replace[escape]
300            except:
301                result = result + c
302            escape = False
303
304    return result
305
306def get_localized_text(filename, id):
307    try:
308        str = localization_data['%s#%s'% (filename, id)]
309    except:
310        return ''
311
312    return unescape(str)
313
314def href_to_fname_id(href):
315    link = href.replace('"', '')
316    fname = link
317    id = ''
318    if link.find("#") >= 0:
319        fname = link[:link.find("#")]
320        id = link[link.find("#")+1:]
321    else:
322        sys.stderr.write('Reference without a "#" in "%s".'% link)
323
324    return [fname, id]
325
326# Exception classes
327class UnhandledItemType(Exception):
328    pass
329# Base class for all the elements
330#
331# self.name - name of the element, to drop the self.child_parsing flag
332# self.objects - collects the child objects that are constructed during
333#                parsing of the child elements
334# self.child_parsing - flag whether we are parsing a child, or the object
335#                      itself
336# self.parent - parent object
337class ElementBase:
338    def __init__(self, name, parent):
339        self.name = name
340        self.objects = []
341        self.child_parsing = False
342        self.parent = parent
343
344    def start_element(self, parser, name, attrs):
345        pass
346
347    def end_element(self, parser, name):
348        if name == self.name:
349            self.parent.child_parsing = False
350
351    def char_data(self, parser, data):
352        pass
353
354    def get_curobj(self):
355        if self.child_parsing:
356            return self.objects[len(self.objects)-1].get_curobj()
357        return self
358
359    # start parsing a child element
360    def parse_child(self, child):
361        self.child_parsing = True
362        self.objects.append(child)
363
364    # construct the wiki representation of this object, including the objects
365    # held in self.objects (here only the text of the objects)
366    def get_all(self):
367        text = u''
368        for i in self.objects:
369            text = text + i.get_all()
370        return text
371
372    # for handling variables, and embedding in general
373    # id - the variable name we want to get
374    def get_variable(self, id):
375        for i in self.objects:
376            if i != None:
377                var = i.get_variable(id)
378                if var != None:
379                    return var
380        return None
381
382    # embed part of another file into current structure
383    def embed_href(self, parent_parser, fname, id):
384        # parse another xhp
385        parser = XhpParser('source/' + fname, False, \
386                parent_parser.current_app, parent_parser.wiki_page_name, \
387                parent_parser.lang)
388        var = parser.get_variable(id)
389
390        if var != None:
391            try:
392                if var.role == 'variable':
393                    var.role = 'paragraph'
394            except:
395                pass
396            self.objects.append(var)
397        elif parser.follow_embed:
398            sys.stderr.write('Cannot find reference "#%s" in "%s".\n'% \
399                    (id, fname))
400
401    def unhandled_element(self, parser, name):
402        sys.stderr.write('Warning: Unhandled element "%s" in "%s" (%s)\n'% \
403                        (name, self.name, parser.filename))
404
405# Base class for trivial elements that operate on char_data
406#
407# Like <comment>, or <title>
408class TextElementBase(ElementBase):
409    def __init__(self, attrs, parent, element_name, start, end, templ):
410        ElementBase.__init__(self, element_name, parent)
411        self.text = u''
412        self.start = start
413        self.end = end
414        self.templ = templ
415
416    def char_data(self, parser, data):
417        self.text = self.text + data
418
419    def get_all(self):
420        if self.templ:
421            return self.start + escape_equals_sign(replace_text(self.text)) + self.end
422        else:
423            return self.start + replace_text(self.text) + self.end
424
425class XhpFile(ElementBase):
426    def __init__(self):
427        ElementBase.__init__(self, None, None)
428
429    def start_element(self, parser, name, attrs):
430        if name == 'body':
431            # ignored, we flatten the structure
432            pass
433        elif name == 'bookmark':
434            self.parse_child(Bookmark(attrs, self, 'div', parser))
435        elif name == 'comment':
436            self.parse_child(Comment(attrs, self))
437        elif name == 'embed' or name == 'embedvar':
438            if parser.follow_embed:
439                (fname, id) = href_to_fname_id(attrs['href'])
440                self.embed_href(parser, fname, id)
441        elif name == 'helpdocument':
442            # ignored, we flatten the structure
443            pass
444        elif name == 'list':
445            self.parse_child(List(attrs, self, False))
446        elif name == 'meta':
447            self.parse_child(Meta(attrs, self))
448        elif name == 'paragraph':
449            parser.parse_paragraph(attrs, self)
450        elif name == 'section':
451            self.parse_child(Section(attrs, self))
452        elif name == 'sort':
453            self.parse_child(Sort(attrs, self))
454        elif name == 'switch':
455            self.parse_child(Switch(attrs, self, parser.embedding_app))
456        elif name == 'table':
457            self.parse_child(Table(attrs, self))
458        elif name == 'bascode':
459            self.parse_child(BasicCode(attrs, self))
460        else:
461            self.unhandled_element(parser, name)
462
463class Bookmark(ElementBase):
464    def __init__(self, attrs, parent, type, parser):
465        ElementBase.__init__(self, 'bookmark', parent)
466
467        self.type = type
468
469        self.id = attrs['id']
470        self.app = ''
471        self.redirect = ''
472        self.target = ''
473        self.authoritative = False
474
475        # let's construct the name of the redirect, so that we can point
476        # to the wikihelp directly from the LO code; wiki then takes care of
477        # the correct redirect
478        branch = attrs['branch']
479        if branch.find('hid/') == 0 and (parser.current_app_raw != '' or parser.follow_embed):
480            name = branch[branch.find('/') + 1:]
481
482            self.app = parser.current_app_raw
483            self.target = parser.wiki_page_name
484            self.authoritative = parser.follow_embed
485            self.redirect = name.replace("/", "%2F")
486
487    def get_all(self):
488        global redirects
489        # first of all, we need to create a redirect page for this one
490        if self.redirect != '' and self.target != '':
491            redirects.append([self.app, self.redirect, \
492                '%s#%s'% (self.target, self.id), \
493                self.authoritative])
494
495        # then we also have to setup ID inside the page
496        if self.type == 'div':
497            return '<div id="%s"></div>\n'% self.id
498        elif self.type == 'span':
499            return '<span id="%s"></span>'% self.id
500        else:
501            sys.stderr.write('Unknown bookmark type "%s"'% self.type)
502
503        return ''
504
505class Image(ElementBase):
506    def __init__(self, attrs, parent):
507        ElementBase.__init__(self, 'image', parent)
508        self.src     = attrs['src']
509        self.align   = 'left'
510        self.alt     = False
511        self.alttext = ""
512
513    def start_element(self, parser, name, attrs):
514        if name == 'alt':
515            self.alt = True
516        else:
517            self.unhandled_element(parser, name)
518
519    def end_element(self, parser, name):
520        ElementBase.end_element(self, parser, name)
521
522        if name == 'alt':
523            self.alt = False
524
525    def char_data(self, parser, data):
526        if self.alt:
527            self.alttext = self.alttext + data
528
529    def get_all(self):
530        global images
531        images.add(self.src)
532
533        name = self.src[self.src.rfind('/') + 1:]
534        wikitext = "[[Image:"+name+"|border|"+self.align+"|"
535        wikitext = wikitext + self.alttext+"]]"
536        return wikitext
537
538    def get_curobj(self):
539        return self
540
541class Br(TextElementBase):
542    def __init__(self, attrs, parent):
543        TextElementBase.__init__(self, attrs, parent, 'br', '<br/>', '', False)
544
545class Comment(TextElementBase):
546    def __init__(self, attrs, parent):
547        TextElementBase.__init__(self, attrs, parent, 'comment', '<!-- ', ' -->', False)
548
549class HelpIdMissing(TextElementBase):
550    def __init__(self, attrs, parent):
551        TextElementBase.__init__(self, attrs, parent, 'help-id-missing', '{{MissingHelpId}}', '', False)
552
553class Text:
554    def __init__(self, text):
555        self.wikitext = replace_text(text)
556
557    def get_all(self):
558        return self.wikitext
559
560    def get_variable(self, id):
561        return None
562
563class TableCell(ElementBase):
564    def __init__(self, attrs, parent):
565        ElementBase.__init__(self, 'tablecell', parent)
566        self.cellHasChildElement = False
567
568    def start_element(self, parser, name, attrs):
569        self.cellHasChildElement = True
570        if name == 'bookmark':
571            self.parse_child(Bookmark(attrs, self, 'div', parser))
572        elif name == 'comment':
573            self.parse_child(Comment(attrs, self))
574        elif name == 'embed' or name == 'embedvar':
575            (fname, id) = href_to_fname_id(attrs['href'])
576            if parser.follow_embed:
577                self.embed_href(parser, fname, id)
578        elif name == 'paragraph':
579            parser.parse_localized_paragraph(TableContentParagraph, attrs, self)
580        elif name == 'section':
581            self.parse_child(Section(attrs, self))
582        elif name == 'bascode':
583            # ignored, do not syntax highlight in table cells
584            pass
585        elif name == 'list':
586            self.parse_child(List(attrs, self, True))
587        else:
588            self.unhandled_element(parser, name)
589
590    def get_all(self):
591        text = ''
592        if not self.cellHasChildElement: # an empty element
593            if self.parent.isTableHeader: # get from TableRow Element
594                role = 'tablehead'
595            else:
596                role = 'tablecontent'
597            text = text + replace_paragraph_role['start'][role]
598            text = text + replace_paragraph_role['end'][role]
599        text = text + ElementBase.get_all(self)
600        return text
601
602class TableRow(ElementBase):
603    def __init__(self, attrs, parent):
604        ElementBase.__init__(self, 'tablerow', parent)
605
606    def start_element(self, parser, name, attrs):
607        self.isTableHeader = False
608        if name == 'tablecell':
609            self.parse_child(TableCell(attrs, self))
610        else:
611            self.unhandled_element(parser, name)
612
613    def get_all(self):
614        text = '|-\n' + ElementBase.get_all(self)
615        return text
616
617class BasicCode(ElementBase):
618    def __init__(self, attrs, parent):
619        ElementBase.__init__(self, 'bascode', parent)
620
621    def start_element(self, parser, name, attrs):
622        if name == 'paragraph':
623            parser.parse_localized_paragraph(BasicCodeParagraph, attrs, self)
624        else:
625            self.unhandled_element(parser, name)
626
627    def get_all(self):
628        text = '<source lang="oobas">\n' + ElementBase.get_all(self) + '</source>\n\n'
629        return text
630
631class Table(ElementBase):
632    def __init__(self, attrs, parent):
633        ElementBase.__init__(self, 'table', parent)
634
635    def start_element(self, parser, name, attrs):
636        if name == 'comment':
637            self.parse_child(Comment(attrs, self))
638        elif name == 'tablerow':
639            self.parse_child(TableRow(attrs, self))
640        else:
641            self.unhandled_element(parser, name)
642
643    def get_all(self):
644        # + ' align="left"' etc.?
645        text = '{| class="wikitable"\n' + \
646            ElementBase.get_all(self) + \
647            '|}\n\n'
648        return text
649
650class ListItem(ElementBase):
651    def __init__(self, attrs, parent):
652        ElementBase.__init__(self, 'listitem', parent)
653
654    def start_element(self, parser, name, attrs):
655        if name == 'bookmark':
656            self.parse_child(Bookmark(attrs, self, 'span', parser))
657        elif name == 'embed' or name == 'embedvar':
658            (fname, id) = href_to_fname_id(attrs['href'])
659            if parser.follow_embed:
660                self.embed_href(parser, fname, id)
661        elif name == 'paragraph':
662            parser.parse_localized_paragraph(ListItemParagraph, attrs, self)
663        elif name == 'list':
664            self.parse_child(List(attrs, self, False))
665        else:
666            self.unhandled_element(parser, name)
667
668    def get_all(self):
669        text = '*'
670        postfix = '\n'
671        if self.parent.startwith > 0:
672            text = '<li>'
673            postfix = '</li>'
674        elif self.parent.type == 'ordered':
675            text = '#'
676
677        # add the text itself
678        linebreak = False
679        for i in self.objects:
680            if linebreak:
681                text = text + '<br/>'
682            ti = i.get_all()
683            # when the object is another list (i.e. nested lists), only the first item
684            # gets the '#' sign in the front by the previous statement
685            # the below re.sub inserts the extra '#' for all additional items of the list
686            ti = re.sub(r'\n\s*#', '\n##', ti)
687            text = text + ti
688            linebreak = True
689
690        return text + postfix
691
692class List(ElementBase):
693    def __init__(self, attrs, parent, isInTable):
694        ElementBase.__init__(self, 'list', parent)
695
696        self.isInTable = isInTable
697        self.type = attrs['type']
698        try:
699            self.startwith = int(attrs['startwith'])
700        except:
701            self.startwith = 0
702
703    def start_element(self, parser, name, attrs):
704        if name == 'listitem':
705            self.parse_child(ListItem(attrs, self))
706        else:
707            self.unhandled_element(parser, name)
708
709    def get_all(self):
710        text = ""
711        if self.isInTable:
712            text = '| |\n'
713        if self.startwith > 0:
714            text = text + '<ol start="%d">\n'% self.startwith
715
716        text = text + ElementBase.get_all(self)
717
718        if self.startwith > 0:
719            text = text + '\n</ol>\n'
720        else:
721            text = text + '\n'
722        return text
723
724# To handle elements that should be completely ignored
725class Ignore(ElementBase):
726    def __init__(self, attrs, parent, element_name):
727        ElementBase.__init__(self, element_name, parent)
728
729class OrigTitle(TextElementBase):
730    def __init__(self, attrs, parent):
731        TextElementBase.__init__(self, attrs, parent, 'title', '{{OrigLang|', '}}\n', True)
732
733class Title(TextElementBase):
734    def __init__(self, attrs, parent, localized_title):
735        TextElementBase.__init__(self, attrs, parent, 'title', '{{Lang|', '}}\n', True)
736        self.localized_title = localized_title
737
738    def get_all(self):
739        if self.localized_title != '':
740            self.text = self.localized_title
741        return TextElementBase.get_all(self)
742
743class Topic(ElementBase):
744    def __init__(self, attrs, parent):
745        ElementBase.__init__(self, 'topic', parent)
746
747    def start_element(self, parser, name, attrs):
748        if name == 'title':
749            if parser.lang == '':
750                self.parse_child(OrigTitle(attrs, self))
751            else:
752                self.parse_child(Title(attrs, self, get_localized_text(parser.filename, 'tit')))
753        elif name == 'filename':
754            self.parse_child(Ignore(attrs, self, name))
755        else:
756            self.unhandled_element(parser, name)
757
758class Meta(ElementBase):
759    def __init__(self, attrs, parent):
760        ElementBase.__init__(self, 'meta', parent)
761
762    def start_element(self, parser, name, attrs):
763        if name == 'topic':
764            self.parse_child(Topic(attrs, self))
765        elif name == 'history':
766            self.parse_child(Ignore(attrs, self, name))
767        else:
768            self.unhandled_element(parser, name)
769
770class Section(ElementBase):
771    def __init__(self, attrs, parent):
772        ElementBase.__init__(self, 'section', parent)
773        self.id = attrs[ 'id' ]
774
775    def start_element(self, parser, name, attrs):
776        if name == 'bookmark':
777            self.parse_child(Bookmark(attrs, self, 'div', parser))
778        elif name == 'comment':
779            self.parse_child(Comment(attrs, self))
780        elif name == 'embed' or name == 'embedvar':
781            (fname, id) = href_to_fname_id(attrs['href'])
782            if parser.follow_embed:
783                self.embed_href(parser, fname, id)
784        elif name == 'list':
785            self.parse_child(List(attrs, self, False))
786        elif name == 'paragraph':
787            parser.parse_paragraph(attrs, self)
788        elif name == 'section':
789            # sections can be nested
790            self.parse_child(Section(attrs, self))
791        elif name == 'switch':
792            self.parse_child(Switch(attrs, self, parser.embedding_app))
793        elif name == 'table':
794            self.parse_child(Table(attrs, self))
795        elif name == 'bascode':
796            self.parse_child(BasicCode(attrs, self))
797        else:
798            self.unhandled_element(parser, name)
799
800    def get_all(self):
801        mapping = ''
802        try:
803            mapping = section_id_mapping[self.id]
804        except:
805            pass
806
807        # some of the section ids are used as real id's, some of them have
808        # function (like relatetopics), and have to be templatized
809        text = ''
810        if mapping != '':
811            text = '{{%s|%s}}\n\n'% (mapping, \
812                    escape_equals_sign(ElementBase.get_all(self)))
813        else:
814            text = ElementBase.get_all(self)
815
816        return text
817
818    def get_variable(self, id):
819        var = ElementBase.get_variable(self, id)
820        if var != None:
821            return var
822        if id == self.id:
823            return self
824        return None
825
826class Sort(ElementBase):
827    def __init__(self, attrs, parent):
828        ElementBase.__init__(self, 'sort', parent)
829
830        try:
831            self.order = attrs['order']
832        except:
833            self.order = 'asc'
834
835    def start_element(self, parser, name, attrs):
836        if name == 'section':
837            self.parse_child(Section(attrs, self))
838        else:
839            self.unhandled_element(parser, name)
840
841    def get_all(self):
842        rev = False
843        if self.order == 'asc':
844            rev = True
845        self.objects = sorted(self.objects, key=lambda obj: obj.id, reverse=rev)
846
847        return ElementBase.get_all(self)
848
849class Link(ElementBase):
850    def __init__(self, attrs, parent, lang):
851        ElementBase.__init__(self, 'link', parent)
852
853        self.link = attrs['href']
854        try:
855            self.lname = attrs['name']
856        except:
857            self.lname = self.link[self.link.rfind("/")+1:]
858        # Override lname
859        self.default_name = self.lname
860        (self.lname, self.fragment) = get_link_filename(self.link, self.lname)
861        self.wikitext = ""
862        self.lang = lang
863
864    def char_data(self, parser, data):
865        self.wikitext = self.wikitext + data
866
867    def get_all(self):
868        if self.wikitext == "":
869            self.wikitext = self.default_name
870
871        self.wikitext = replace_text(self.wikitext)
872        if self.link.find("http") == 0:
873            text = '[%s %s]'% (self.link, self.wikitext)
874        elif self.lang != '':
875            text = '[[%s/%s%s|%s]]'% (self.lname, self.lang, self.fragment, self.wikitext)
876        else:
877            text = '[[%s%s|%s]]'% (self.lname, self.fragment, self.wikitext)
878        return text
879
880class SwitchInline(ElementBase):
881    def __init__(self, attrs, parent, app):
882        ElementBase.__init__(self, 'switchinline', parent)
883        self.switch = attrs['select']
884        self.embedding_app = app
885
886    def start_element(self, parser, name, attrs):
887        if name == 'caseinline':
888            self.parse_child(CaseInline(attrs, self, False))
889        elif name == 'defaultinline':
890            self.parse_child(CaseInline(attrs, self, True))
891        else:
892            self.unhandled_element(parser, name)
893
894    def get_all(self):
895        if len(self.objects) == 0:
896            return ''
897        elif self.switch == 'sys':
898            system = {'MAC':'', 'UNIX':'', 'WIN':'', 'default':''}
899            for i in self.objects:
900                if i.case == 'MAC' or i.case == 'UNIX' or \
901                   i.case == 'WIN' or i.case == 'default':
902                    system[i.case] = i.get_all()
903                elif i.case == 'OS2':
904                    # ignore, there is only one mention of OS2, which is a
905                    # 'note to translators', and no meat
906                    pass
907                elif i.case == 'HIDE_HERE':
908                    # do what the name suggest ;-)
909                    pass
910                else:
911                    sys.stderr.write('Unhandled "%s" case in "sys" switchinline.\n'% \
912                            i.case )
913            text = '{{System'
914            for i in [['default', 'default'], ['MAC', 'mac'], \
915                      ['UNIX', 'unx'], ['WIN', 'win']]:
916                if system[i[0]] != '':
917                    text = '%s|%s=%s'% (text, i[1], system[i[0]])
918            return text + '}}'
919        elif self.switch == 'appl':
920            # we want directly use the right text, when inlining something
921            # 'shared' into an 'app'
922            if self.embedding_app == '':
923                text = ''
924                default = ''
925                for i in self.objects:
926                    appls = {'BASIC':'Basic', 'CALC':'Calc', \
927                             'CHART':'Chart', 'DRAW':'Draw', \
928                             'IMAGE':'Draw', 'IMPRESS': 'Impress', \
929                             'MATH':'Math', 'WRITER':'Writer', \
930                             'OFFICE':'', 'default':''}
931                    try:
932                        app = appls[i.case]
933                        all = i.get_all()
934                        if all == '':
935                            pass
936                        elif app == '':
937                            default = all
938                        else:
939                            text = text + '{{WhenIn%s|%s}}'% (app, escape_equals_sign(all))
940                    except:
941                        sys.stderr.write('Unhandled "%s" case in "appl" switchinline.\n'% \
942                                i.case)
943
944                if text == '':
945                    text = default
946                elif default != '':
947                    text = text + '{{WhenDefault|%s}}'% escape_equals_sign(default)
948
949                return text
950            else:
951                for i in self.objects:
952                    if i.case == self.embedding_app:
953                        return i.get_all()
954
955        return ''
956
957class Case(ElementBase):
958    def __init__(self, attrs, parent, is_default):
959        ElementBase.__init__(self, 'case', parent)
960
961        if is_default:
962            self.name = 'default'
963            self.case = 'default'
964        else:
965            self.case = attrs['select']
966
967    def start_element(self, parser, name, attrs):
968        if name == 'bookmark':
969            self.parse_child(Bookmark(attrs, self, 'div', parser))
970        elif name == 'comment':
971            self.parse_child(Comment(attrs, self))
972        elif name == 'embed' or name == 'embedvar':
973            if parser.follow_embed:
974                (fname, id) = href_to_fname_id(attrs['href'])
975                self.embed_href(parser, fname, id)
976        elif name == 'list':
977            self.parse_child(List(attrs, self, False))
978        elif name == 'paragraph':
979            parser.parse_paragraph(attrs, self)
980        elif name == 'section':
981            self.parse_child(Section(attrs, self))
982        elif name == 'table':
983            self.parse_child(Table(attrs, self))
984        elif name == 'bascode':
985            self.parse_child(BasicCode(attrs, self))
986        else:
987            self.unhandled_element(parser, name)
988
989class Switch(SwitchInline):
990    def __init__(self, attrs, parent, app):
991        SwitchInline.__init__(self, attrs, parent, app)
992        self.name = 'switch'
993
994    def start_element(self, parser, name, attrs):
995        self.embedding_app = parser.embedding_app
996        if name == 'case':
997            self.parse_child(Case(attrs, self, False))
998        elif name == 'default':
999            self.parse_child(Case(attrs, self, True))
1000        else:
1001            self.unhandled_element(parser, name)
1002
1003class Item(ElementBase):
1004    replace_type = \
1005            {'start':{'acronym' : '\'\'',
1006                      'code': '<code>',
1007                      'input': '<code>',
1008                      'keycode': '{{KeyCode|',
1009                      'tasto': '{{KeyCode|',
1010                      'litera': '<code>',
1011                      'literal': '<code>',
1012                      'menuitem': '{{MenuItem|',
1013                      'mwnuitem': '{{MenuItem|',
1014                      'OpenOffice.org': '',
1015                      'productname': '',
1016                      'unknown': '<code>'
1017                     },
1018             'end':{'acronym' : '\'\'',
1019                    'code': '</code>',
1020                    'input': '</code>',
1021                    'keycode': '}}',
1022                    'tasto': '}}',
1023                    'litera': '</code>',
1024                    'literal': '</code>',
1025                    'menuitem': '}}',
1026                    'mwnuitem': '}}',
1027                    'OpenOffice.org': '',
1028                    'productname': '',
1029                    'unknown': '</code>'
1030                   },
1031             'templ':{'acronym': False,
1032                      'code': False,
1033                      'input': False,
1034                      'keycode': True,
1035                      'tasto': True,
1036                      'litera': False,
1037                      'literal': False,
1038                      'menuitem': True,
1039                      'mwnuitem': True,
1040                      'OpenOffice.org': False,
1041                      'productname': False,
1042                      'unknown': False
1043                     }}
1044
1045    def __init__(self, attrs, parent):
1046        ElementBase.__init__(self, 'item', parent)
1047
1048        try:
1049            self.type = attrs['type']
1050        except:
1051            self.type = 'unknown'
1052        self.text = ''
1053
1054    def char_data(self, parser, data):
1055        self.text = self.text + data
1056
1057    def get_all(self):
1058        try:
1059            text = ''
1060            if self.replace_type['templ'][self.type]:
1061                text = escape_equals_sign(replace_text(self.text))
1062            else:
1063                text = replace_text(self.text)
1064            return self.replace_type['start'][self.type] + \
1065                   text + \
1066                   self.replace_type['end'][self.type]
1067        except:
1068            try:
1069                sys.stderr.write('Unhandled item type "%s".\n'% self.type)
1070            except:
1071                sys.stderr.write('Unhandled item type. Possibly type has been localized.\n')
1072            finally:
1073                raise UnhandledItemType
1074
1075class Paragraph(ElementBase):
1076    def __init__(self, attrs, parent):
1077        ElementBase.__init__(self, 'paragraph', parent)
1078
1079        try:
1080            self.role = attrs['role']
1081        except:
1082            self.role = 'paragraph'
1083
1084        try:
1085            self.id = attrs['id']
1086        except:
1087            self.id = ""
1088
1089        try:
1090            self.level = int(attrs['level'])
1091        except:
1092            self.level = 0
1093
1094        self.is_first = (len(self.parent.objects) == 0)
1095
1096    def start_element(self, parser, name, attrs):
1097        if name == 'ahelp':
1098            try:
1099                if attrs['visibility'] == 'hidden':
1100                    self.parse_child(Ignore(attrs, self, name))
1101            except:
1102                pass
1103        elif name == 'br':
1104            self.parse_child(Br(attrs, self))
1105        elif name == 'comment':
1106            self.parse_child(Comment(attrs, self))
1107        elif name == 'emph':
1108            self.parse_child(Emph(attrs, self))
1109        elif name == 'sup':
1110            self.parse_child(Sup(attrs, self))
1111        elif name == 'embedvar':
1112            if parser.follow_embed:
1113                (fname, id) = href_to_fname_id(attrs['href'])
1114                self.embed_href(parser, fname, id)
1115        elif name == 'help-id-missing':
1116            self.parse_child(HelpIdMissing(attrs, self))
1117        elif name == 'image':
1118            self.parse_child(Image(attrs, self))
1119        elif name == 'item':
1120            self.parse_child(Item(attrs, self))
1121        elif name == 'link':
1122            self.parse_child(Link(attrs, self, parser.lang))
1123        elif name == 'localized':
1124            # we ignore this tag, it is added arbitrary for the paragraphs
1125            # that come from .sdf files
1126            pass
1127        elif name == 'switchinline':
1128            self.parse_child(SwitchInline(attrs, self, parser.embedding_app))
1129        elif name == 'variable':
1130            self.parse_child(Variable(attrs, self))
1131        else:
1132            self.unhandled_element(parser, name)
1133
1134    def char_data(self, parser, data):
1135        if self.role == 'paragraph' or self.role == 'heading' or \
1136                self.role == 'listitem' or self.role == 'variable':
1137            if data != '' and data[0] == ' ':
1138                data = ' ' + data.lstrip()
1139            data = data.replace('\n', ' ')
1140
1141        if len(data):
1142            self.objects.append(Text(data))
1143
1144    def get_all(self):
1145        role = self.role
1146        if role == 'heading':
1147            if self.level <= 0:
1148                sys.stderr.write('Heading, but the level is %d.\n'% self.level)
1149            elif self.level < 6:
1150                role = 'heading%d'% self.level
1151            else:
1152                role = 'heading6'
1153
1154        # if we are not the first para in the table, we need special handling
1155        if not self.is_first and role.find('table') == 0:
1156            if role == 'tablecontentcode':
1157                role = 'tablenextparacode'
1158            elif role == 'tablecontentnote':
1159                role = 'tablenextnote'
1160            elif role == 'tablecontenttip':
1161                role = 'tablenexttip'
1162            elif role == 'tablecontentwarning':
1163                role = 'tablenextwarning'
1164            else:
1165                role = 'tablenextpara'
1166
1167        # the text itself
1168        try:
1169            children = ElementBase.get_all(self)
1170        except UnhandledItemType:
1171            raise UnhandledItemType('Paragraph id: '+str(self.id))
1172        if self.role != 'emph' and self.role != 'bascode' and self.role != 'logocode':
1173            children = children.strip()
1174
1175        if len(children) == 0:
1176            return ''
1177
1178        # prepend the markup according to the role
1179        text = ''
1180        try:
1181            text = text + replace_paragraph_role['start'][role]
1182        except:
1183            sys.stderr.write( "Unknown paragraph role start: " + role + "\n" )
1184
1185        if replace_paragraph_role['templ'][role]:
1186            text = text + escape_equals_sign(children)
1187        else:
1188            text = text + children
1189
1190        # append the markup according to the role
1191        try:
1192            text = text + replace_paragraph_role['end'][role]
1193        except:
1194            sys.stderr.write( "Unknown paragraph role end: " + role + "\n" )
1195
1196        return text
1197
1198class Variable(Paragraph):
1199    def __init__(self, attrs, parent):
1200        Paragraph.__init__(self, attrs, parent)
1201        self.name = 'variable'
1202        self.role = 'variable'
1203        self.id = attrs['id']
1204
1205    def get_variable(self, id):
1206        if id == self.id:
1207            return self
1208        return None
1209
1210class CaseInline(Paragraph):
1211    def __init__(self, attrs, parent, is_default):
1212        Paragraph.__init__(self, attrs, parent)
1213
1214        self.role = 'null'
1215        if is_default:
1216            self.name = 'defaultinline'
1217            self.case = 'default'
1218        else:
1219            self.name = 'caseinline'
1220            self.case = attrs['select']
1221
1222class Emph(Paragraph):
1223    def __init__(self, attrs, parent):
1224        Paragraph.__init__(self, attrs, parent)
1225        self.name = 'emph'
1226        self.role = 'emph'
1227
1228    def get_all(self):
1229        text = Paragraph.get_all(self)
1230        if len(text):
1231            return "'''" + text + "'''"
1232        return ''
1233
1234class Sup(Paragraph):
1235    def __init__(self, attrs, parent):
1236        Paragraph.__init__(self, attrs, parent)
1237        self.name = 'sup'
1238        self.role = 'sup'
1239
1240    def get_all(self):
1241        text = Paragraph.get_all(self)
1242        if len(text):
1243            return "<sup>" + text + "</sup>"
1244        return ''
1245
1246class ListItemParagraph(Paragraph):
1247    def __init__(self, attrs, parent):
1248        Paragraph.__init__(self, attrs, parent)
1249        self.role = 'listitem'
1250
1251class BasicCodeParagraph(Paragraph):
1252    def __init__(self, attrs, parent):
1253        Paragraph.__init__(self, attrs, parent)
1254        self.role = 'bascode'
1255
1256class TableContentParagraph(Paragraph):
1257    def __init__(self, attrs, parent):
1258        Paragraph.__init__(self, attrs, parent)
1259        if self.role != 'tablehead' and self.role != 'tablecontent':
1260            if self.role == 'code':
1261                self.role = 'tablecontentcode'
1262            elif self.role == 'bascode':
1263                self.role = 'tablecontentcode'
1264            elif self.role == 'logocode':
1265                self.role = 'tablecontentcode'
1266            elif self.role == 'note':
1267                self.role = 'tablecontentnote'
1268            elif self.role == 'tip':
1269                self.role = 'tablecontenttip'
1270            elif self.role == 'warning':
1271                self.role = 'tablecontentwarning'
1272            else:
1273                self.role = 'tablecontent'
1274        if self.role == 'tablehead':
1275            self.parent.parent.isTableHeader = True # self.parent.parent is TableRow Element
1276        else:
1277            self.parent.parent.isTableHeader = False
1278
1279class ParserBase:
1280    def __init__(self, filename, follow_embed, embedding_app, current_app, wiki_page_name, lang, head_object, buffer):
1281        self.filename = filename
1282        self.follow_embed = follow_embed
1283        self.embedding_app = embedding_app
1284        self.current_app = current_app
1285        self.wiki_page_name = wiki_page_name
1286        self.lang = lang
1287        self.head_obj = head_object
1288
1289        p = xml.parsers.expat.ParserCreate()
1290        p.StartElementHandler = self.start_element
1291        p.EndElementHandler = self.end_element
1292        p.CharacterDataHandler = self.char_data
1293
1294        p.Parse(buffer)
1295
1296    def start_element(self, name, attrs):
1297        self.head_obj.get_curobj().start_element(self, name, attrs)
1298
1299    def end_element(self, name):
1300        self.head_obj.get_curobj().end_element(self, name)
1301
1302    def char_data(self, data):
1303        self.head_obj.get_curobj().char_data(self, data)
1304
1305    def get_all(self):
1306        return self.head_obj.get_all()
1307
1308    def get_variable(self, id):
1309        return self.head_obj.get_variable(id)
1310
1311    def parse_localized_paragraph(self, Paragraph_type, attrs, obj):
1312        localized_text = ''
1313        try:
1314            localized_text = get_localized_text(self.filename, attrs['id'])
1315        except:
1316            pass
1317
1318        paragraph = Paragraph_type(attrs, obj)
1319        if localized_text != '':
1320            # parse the localized text
1321            text = u'<?xml version="1.0" encoding="UTF-8"?><localized>' + localized_text + '</localized>'
1322            try:
1323                ParserBase(self.filename, self.follow_embed, self.embedding_app, \
1324                        self.current_app, self.wiki_page_name, self.lang, \
1325                        paragraph, text.encode('utf-8'))
1326            except xml.parsers.expat.ExpatError:
1327                sys.stderr.write( 'Invalid XML in translated text. Using the original text. Error location:\n'\
1328                                  + 'Current xhp: ' + self.filename + '\nParagraph id: ' + attrs['id'] + '\n')
1329                obj.parse_child(Paragraph_type(attrs, obj)) # new paragraph must be created because "paragraph" is corrupted by "ParserBase"
1330            else:
1331                # add it to the overall structure
1332                obj.objects.append(paragraph)
1333                # and ignore the original text
1334                obj.parse_child(Ignore(attrs, obj, 'paragraph'))
1335        else:
1336            obj.parse_child(paragraph)
1337
1338    def parse_paragraph(self, attrs, obj):
1339        ignore_this = False
1340        try:
1341            if attrs['role'] == 'heading' and int(attrs['level']) == 1 \
1342                    and self.ignore_heading and self.follow_embed:
1343                self.ignore_heading = False
1344                ignore_this = True
1345        except:
1346            pass
1347
1348        if ignore_this:
1349            obj.parse_child(Ignore(attrs, obj, 'paragraph'))
1350        else:
1351            self.parse_localized_paragraph(Paragraph, attrs, obj)
1352
1353class XhpParser(ParserBase):
1354    def __init__(self, filename, follow_embed, embedding_app, wiki_page_name, lang):
1355        # we want to ignore the 1st level="1" heading, because in most of the
1356        # cases, it is the only level="1" heading in the file, and it is the
1357        # same as the page title
1358        self.ignore_heading = True
1359
1360        current_app = ''
1361        self.current_app_raw = ''
1362        for i in [['sbasic', 'BASIC'], ['scalc', 'CALC'], \
1363                  ['sdatabase', 'DATABASE'], ['sdraw', 'DRAW'], \
1364                  ['schart', 'CHART'], ['simpress', 'IMPRESS'], \
1365                  ['smath', 'MATH'], ['swriter', 'WRITER']]:
1366            if filename.find('/%s/'% i[0]) >= 0:
1367                self.current_app_raw = i[0]
1368                current_app = i[1]
1369                break
1370
1371        if embedding_app == '':
1372            embedding_app = current_app
1373
1374        file = codecs.open(filename, "r", "utf-8")
1375        buf = file.read()
1376        file.close()
1377
1378        ParserBase.__init__(self, filename, follow_embed, embedding_app,
1379                current_app, wiki_page_name, lang, XhpFile(), buf.encode('utf-8'))
1380
1381class WikiConverter(Thread):
1382    def __init__(self, inputfile, wiki_page_name, lang, outputfile):
1383        Thread.__init__(self)
1384        self.inputfile = inputfile
1385        self.wiki_page_name = wiki_page_name
1386        self.lang = lang
1387        self.outputfile = outputfile
1388
1389    def run(self):
1390        parser = XhpParser(self.inputfile, True, '', self.wiki_page_name, self.lang)
1391        file = codecs.open(self.outputfile, "wb", "utf-8")
1392        file.write(parser.get_all())
1393        file.close()
1394
1395def write_link(r, target):
1396    fname = 'wiki/%s'% r
1397    try:
1398        file = open(fname, "w")
1399        file.write('#REDIRECT [[%s]]\n'% target)
1400        file.close()
1401    except:
1402        sys.stderr.write('Unable to write "%s".\n'%'wiki/%s'% fname)
1403
1404def write_redirects():
1405    print 'Generating the redirects...'
1406    written = {}
1407    # in the first pass, immediately write the links that are embedded, so that
1408    # we can always point to that source versions
1409    for redir in redirects:
1410        app = redir[0]
1411        redirect = redir[1]
1412        target = redir[2]
1413        authoritative = redir[3]
1414
1415        if app != '':
1416            r = '%s/%s'% (app, redirect)
1417            if authoritative:
1418                write_link(r, target)
1419                written[r] = True
1420            else:
1421                try:
1422                    written[r]
1423                except:
1424                    written[r] = False
1425
1426    # in the second pass, output the wiki links
1427    for redir in redirects:
1428        app = redir[0]
1429        redirect = redir[1]
1430        target = redir[2]
1431
1432        if app == '':
1433            for i in ['swriter', 'scalc', 'simpress', 'sdraw', 'smath', \
1434                      'schart', 'sbasic', 'sdatabase']:
1435                write_link('%s/%s'% (i, redirect), target)
1436        else:
1437            r = '%s/%s'% (app, redirect)
1438            if not written[r]:
1439                write_link(r, target)
1440
1441# Main Function
1442def convert(title_data, generate_redirects, lang, po_root):
1443    if lang == '':
1444        print 'Generating the main wiki pages...'
1445    else:
1446        print 'Generating the wiki pages for language %s...'% lang
1447
1448    global titles
1449    titles = [t for t in title_data]
1450    global redirects
1451    redirects = []
1452    global images
1453    images = set()
1454
1455    if lang != '':
1456        sys.stderr.write('Using localizations from "%s"\n'% po_root)
1457        if not load_localization_data(po_root):
1458            return
1459
1460    for title in titles:
1461        while threading.active_count() > max_threads:
1462            time.sleep(0.001)
1463
1464        infile = title[0].strip()
1465        wikiname = title[1].strip()
1466        articledir = 'wiki/' + wikiname
1467        try:
1468            os.mkdir(articledir)
1469        except:
1470            pass
1471
1472        outfile = ''
1473        if lang != '':
1474            wikiname = '%s/%s'% (wikiname, lang)
1475            outfile = '%s/%s'% (articledir, lang)
1476        else:
1477            outfile = '%s/MAIN'% articledir
1478
1479        try:
1480            file = open(outfile, 'r')
1481        except:
1482            try:
1483                wiki = WikiConverter(infile, wikiname, lang, outfile)
1484                wiki.start()
1485                continue
1486            except:
1487                print 'Failed to convert "%s" into "%s".\n'% \
1488                        (infile, outfile)
1489        sys.stderr.write('Warning: Skipping: %s > %s\n'% (infile, outfile))
1490        file.close()
1491
1492    # wait for everyone to finish
1493    while threading.active_count() > 1:
1494        time.sleep(0.001)
1495
1496    if lang == '':
1497        # set of the images used here
1498        print 'Generating "images.txt", the list of used images...'
1499        file = open('images.txt', "w")
1500        for image in images:
1501            file.write('%s\n'% image)
1502        file.close()
1503
1504        # generate the redirects
1505        if generate_redirects:
1506            write_redirects()
1507
1508# vim:set shiftwidth=4 softtabstop=4 expandtab:
1509